FLEDGE: Basic diagnostics in devtools console
Basically all steps of the auction process can chose to fill in an Optional<string>, then AuctionRunner collects them, and AdAuctionServiceImpl delivers them to a devtools hook that directly broadcasts them to devtools frontend, bypassing the usual path through the renderer since the messages may contain sensitive cross-origin information. (Also fixes a tiny bug involving double invocation of SendReportTo) Bug: 1186444 Change-Id: If0bf0c7529b00c4da9f88fcdc467ddc959ba291c Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2845592 Commit-Queue: Maksim Orlovich <morlovich@chromium.org> Reviewed-by: Andrey Kosyakov <caseq@chromium.org> Reviewed-by: Matt Menke <mmenke@chromium.org> Reviewed-by: Daniel Cheng <dcheng@chromium.org> Cr-Commit-Position: refs/heads/master@{#879830}
This commit is contained in:

committed by
Chromium LUCI CQ

parent
f5a7e9cf73
commit
ad48c5df9a
content
browser
devtools
interest_group
services
auction_worklet
auction_downloader.ccauction_downloader.hauction_downloader_unittest.ccauction_runner.ccauction_runner.hauction_runner_unittest.ccauction_v8_helper.ccauction_v8_helper.hauction_v8_helper_unittest.ccbidder_worklet.ccbidder_worklet.hbidder_worklet_unittest.cc
public
report_bindings.ccseller_worklet.ccseller_worklet.hseller_worklet_unittest.cctrusted_bidding_signals.cctrusted_bidding_signals.htrusted_bidding_signals_unittest.ccworklet_loader.ccworklet_loader.hworklet_loader_unittest.cc@@ -3,6 +3,7 @@
|
||||
// found in the LICENSE file.
|
||||
#include "content/browser/devtools/devtools_instrumentation.h"
|
||||
|
||||
#include "base/strings/strcat.h"
|
||||
#include "base/strings/stringprintf.h"
|
||||
#include "components/download/public/common/download_create_info.h"
|
||||
#include "components/download/public/common/download_item.h"
|
||||
@@ -982,6 +983,21 @@ void OnWebTransportHandshakeFailed(
|
||||
DispatchToAgents(ftn, &protocol::LogHandler::EntryAdded, entry.get());
|
||||
}
|
||||
|
||||
void LogWorkletError(RenderFrameHostImpl* frame_host,
|
||||
const std::string& error) {
|
||||
FrameTreeNode* ftn = frame_host->frame_tree_node();
|
||||
if (!ftn)
|
||||
return;
|
||||
std::string text = base::StrCat({"Worklet error: ", error});
|
||||
auto entry = protocol::Log::LogEntry::Create()
|
||||
.SetSource(protocol::Log::LogEntry::SourceEnum::Other)
|
||||
.SetLevel(protocol::Log::LogEntry::LevelEnum::Error)
|
||||
.SetText(text)
|
||||
.SetTimestamp(base::Time::Now().ToDoubleT() * 1000.0)
|
||||
.Build();
|
||||
DispatchToAgents(ftn, &protocol::LogHandler::EntryAdded, entry.get());
|
||||
}
|
||||
|
||||
void ApplyNetworkContextParamsOverrides(
|
||||
BrowserContext* browser_context,
|
||||
network::mojom::NetworkContextParams* context_params) {
|
||||
|
@@ -211,6 +211,9 @@ void OnWebTransportHandshakeFailed(
|
||||
const GURL& url,
|
||||
const base::Optional<net::QuicTransportError>& error);
|
||||
|
||||
// Adds a debug error message from a worklet to the devtools console.
|
||||
void LogWorkletError(RenderFrameHostImpl* frame_host, const std::string& error);
|
||||
|
||||
void ApplyNetworkContextParamsOverrides(
|
||||
BrowserContext* browser_context,
|
||||
network::mojom::NetworkContextParams* network_context_params);
|
||||
|
@@ -13,6 +13,7 @@
|
||||
#include "base/memory/weak_ptr.h"
|
||||
#include "base/optional.h"
|
||||
#include "base/strings/stringprintf.h"
|
||||
#include "content/browser/devtools/devtools_instrumentation.h"
|
||||
#include "content/browser/renderer_host/render_frame_host_impl.h"
|
||||
#include "content/browser/service_sandbox_type.h"
|
||||
#include "content/browser/storage_partition_impl.h"
|
||||
@@ -322,10 +323,17 @@ void AdAuctionServiceImpl::WorkletComplete(
|
||||
const url::Origin& owner,
|
||||
const std::string& name,
|
||||
auction_worklet::mojom::WinningBidderReportPtr bidder_report,
|
||||
auction_worklet::mojom::SellerReportPtr seller_report) {
|
||||
auction_worklet::mojom::SellerReportPtr seller_report,
|
||||
const std::vector<std::string>& errors) {
|
||||
// Release process if needed.
|
||||
AuctionComplete();
|
||||
|
||||
// Forward debug information to devtools.
|
||||
for (const std::string& error : errors) {
|
||||
devtools_instrumentation::LogWorkletError(
|
||||
static_cast<RenderFrameHostImpl*>(render_frame_host()), error);
|
||||
}
|
||||
|
||||
// Check if returned winner's information is valid.
|
||||
ValidatedResult result =
|
||||
ValidateAuctionResult(bidders, render_url, owner, name);
|
||||
|
@@ -94,7 +94,8 @@ class CONTENT_EXPORT AdAuctionServiceImpl final
|
||||
const url::Origin& owner,
|
||||
const std::string& name,
|
||||
auction_worklet::mojom::WinningBidderReportPtr bidder_report,
|
||||
auction_worklet::mojom::SellerReportPtr seller_report);
|
||||
auction_worklet::mojom::SellerReportPtr seller_report,
|
||||
const std::vector<std::string>& errors);
|
||||
|
||||
// Returns an untrusted URLLoaderFactory created by the RenderFrameHost,
|
||||
// suitable for loading URLs like subresources. Caches the factory in
|
||||
|
@@ -11,7 +11,10 @@
|
||||
#include "base/bind.h"
|
||||
#include "base/callback.h"
|
||||
#include "base/strings/string_util.h"
|
||||
#include "base/strings/stringprintf.h"
|
||||
#include "net/base/net_errors.h"
|
||||
#include "net/http/http_request_headers.h"
|
||||
#include "net/http/http_status_code.h"
|
||||
#include "net/traffic_annotation/network_traffic_annotation.h"
|
||||
#include "net/url_request/redirect_info.h"
|
||||
#include "services/network/public/cpp/resource_request.h"
|
||||
@@ -87,7 +90,8 @@ AuctionDownloader::AuctionDownloader(
|
||||
const GURL& source_url,
|
||||
MimeType mime_type,
|
||||
AuctionDownloaderCallback auction_downloader_callback)
|
||||
: mime_type_(mime_type),
|
||||
: source_url_(source_url),
|
||||
mime_type_(mime_type),
|
||||
auction_downloader_callback_(std::move(auction_downloader_callback)) {
|
||||
DCHECK(auction_downloader_callback_);
|
||||
auto resource_request = std::make_unique<network::ResourceRequest>();
|
||||
@@ -100,8 +104,6 @@ AuctionDownloader::AuctionDownloader(
|
||||
simple_url_loader_ = network::SimpleURLLoader::Create(
|
||||
std::move(resource_request), kTrafficAnnotation);
|
||||
|
||||
// TODO(mmenke): Reject unexpected charsets.
|
||||
|
||||
// Abort on redirects.
|
||||
// TODO(mmenke): May want a browser-side proxy to block redirects instead.
|
||||
simple_url_loader_->SetOnRedirectCallback(base::BindRepeating(
|
||||
@@ -120,19 +122,54 @@ void AuctionDownloader::OnBodyReceived(std::unique_ptr<std::string> body) {
|
||||
|
||||
auto simple_url_loader = std::move(simple_url_loader_);
|
||||
std::string allow_fledge;
|
||||
if (!body || !simple_url_loader->ResponseInfo()->headers ||
|
||||
!simple_url_loader->ResponseInfo()->headers->GetNormalizedHeader(
|
||||
"X-Allow-FLEDGE", &allow_fledge) ||
|
||||
!base::EqualsCaseInsensitiveASCII(allow_fledge, "true") ||
|
||||
// Note that ResponseInfo's `mime_type` is always lowercase.
|
||||
!MimeTypeIsConsistent(mime_type_,
|
||||
simple_url_loader->ResponseInfo()->mime_type) ||
|
||||
!IsAllowedCharset(simple_url_loader->ResponseInfo()->charset, *body)) {
|
||||
std::move(auction_downloader_callback_).Run(nullptr);
|
||||
return;
|
||||
}
|
||||
|
||||
std::move(auction_downloader_callback_).Run(std::move(body));
|
||||
if (!body) {
|
||||
std::string error_msg;
|
||||
if (simple_url_loader->ResponseInfo() &&
|
||||
simple_url_loader->ResponseInfo()->headers &&
|
||||
simple_url_loader->ResponseInfo()->headers->response_code() / 100 !=
|
||||
2) {
|
||||
int status = simple_url_loader->ResponseInfo()->headers->response_code();
|
||||
error_msg = base::StringPrintf(
|
||||
"Failed to load %s HTTP status = %d %s.", source_url_.spec().c_str(),
|
||||
status,
|
||||
simple_url_loader->ResponseInfo()->headers->GetStatusText().c_str());
|
||||
} else {
|
||||
error_msg = base::StringPrintf(
|
||||
"Failed to load %s error = %s.", source_url_.spec().c_str(),
|
||||
net::ErrorToString(simple_url_loader->NetError()).c_str());
|
||||
}
|
||||
std::move(auction_downloader_callback_).Run(nullptr /* body */, error_msg);
|
||||
} else if (!simple_url_loader->ResponseInfo()->headers ||
|
||||
!simple_url_loader->ResponseInfo()->headers->GetNormalizedHeader(
|
||||
"X-Allow-FLEDGE", &allow_fledge) ||
|
||||
!base::EqualsCaseInsensitiveASCII(allow_fledge, "true")) {
|
||||
std::move(auction_downloader_callback_)
|
||||
.Run(nullptr /* body */,
|
||||
base::StringPrintf(
|
||||
"Rejecting load of %s due to lack of X-Allow-FLEDGE: true.",
|
||||
source_url_.spec().c_str()));
|
||||
} else if (!MimeTypeIsConsistent(
|
||||
mime_type_,
|
||||
// ResponseInfo's `mime_type` is always lowercase.
|
||||
simple_url_loader->ResponseInfo()->mime_type)) {
|
||||
std::move(auction_downloader_callback_)
|
||||
.Run(nullptr /* body */,
|
||||
base::StringPrintf(
|
||||
"Rejecting load of %s due to unexpected MIME type.",
|
||||
source_url_.spec().c_str()));
|
||||
} else if (!IsAllowedCharset(simple_url_loader->ResponseInfo()->charset,
|
||||
*body)) {
|
||||
std::move(auction_downloader_callback_)
|
||||
.Run(nullptr /* body */,
|
||||
base::StringPrintf(
|
||||
"Rejecting load of %s due to unexpected charset.",
|
||||
source_url_.spec().c_str()));
|
||||
} else {
|
||||
// All OK!
|
||||
std::move(auction_downloader_callback_)
|
||||
.Run(std::move(body), base::nullopt /* error_msg */);
|
||||
}
|
||||
}
|
||||
|
||||
void AuctionDownloader::OnRedirect(
|
||||
@@ -144,7 +181,9 @@ void AuctionDownloader::OnRedirect(
|
||||
// Need to cancel the load, to prevent the request from continuing.
|
||||
simple_url_loader_.reset();
|
||||
|
||||
std::move(auction_downloader_callback_).Run(nullptr /* body */);
|
||||
std::move(auction_downloader_callback_)
|
||||
.Run(nullptr /* body */, base::StringPrintf("Unexpected redirect on %s.",
|
||||
source_url_.spec().c_str()));
|
||||
}
|
||||
|
||||
} // namespace auction_worklet
|
||||
|
@@ -9,6 +9,7 @@
|
||||
#include <string>
|
||||
|
||||
#include "base/callback.h"
|
||||
#include "base/optional.h"
|
||||
#include "net/url_request/redirect_info.h"
|
||||
#include "services/network/public/mojom/url_loader_factory.mojom-forward.h"
|
||||
#include "services/network/public/mojom/url_response_head.mojom.h"
|
||||
@@ -32,11 +33,9 @@ class AuctionDownloader {
|
||||
};
|
||||
|
||||
// Passes in nullptr on failure. Always invoked asynchronously.
|
||||
//
|
||||
// TODO(mmenke): Pass along some sort of error string on failure, to make
|
||||
// debugging easier.
|
||||
using AuctionDownloaderCallback =
|
||||
base::OnceCallback<void(std::unique_ptr<std::string> response_body)>;
|
||||
base::OnceCallback<void(std::unique_ptr<std::string> response_body,
|
||||
base::Optional<std::string> error)>;
|
||||
|
||||
// Starts loading the worklet script on construction. Callback will be invoked
|
||||
// asynchronously once the data has been fetched or an error has occurred.
|
||||
@@ -55,6 +54,7 @@ class AuctionDownloader {
|
||||
const network::mojom::URLResponseHead& response_head,
|
||||
std::vector<std::string>* removed_headers);
|
||||
|
||||
const GURL source_url_;
|
||||
const MimeType mime_type_;
|
||||
std::unique_ptr<network::SimpleURLLoader> simple_url_loader_;
|
||||
AuctionDownloaderCallback auction_downloader_callback_;
|
||||
|
@@ -52,23 +52,32 @@ class AuctionDownloaderTest : public testing::Test {
|
||||
return std::move(body_);
|
||||
}
|
||||
|
||||
// Helper to avoid checking has_value all over the place.
|
||||
std::string last_error_msg() const {
|
||||
return error_.value_or("Not an error.");
|
||||
}
|
||||
|
||||
protected:
|
||||
void DownloadCompleteCallback(std::unique_ptr<std::string> body) {
|
||||
void DownloadCompleteCallback(std::unique_ptr<std::string> body,
|
||||
base::Optional<std::string> error) {
|
||||
DCHECK(!body_);
|
||||
DCHECK(run_loop_);
|
||||
body_ = std::move(body);
|
||||
error_ = std::move(error);
|
||||
EXPECT_EQ(error_.has_value(), !body_);
|
||||
run_loop_->Quit();
|
||||
}
|
||||
|
||||
base::test::TaskEnvironment task_environment_;
|
||||
|
||||
const GURL url_ = GURL("https://url.test/");
|
||||
const GURL url_ = GURL("https://url.test/script.js");
|
||||
|
||||
AuctionDownloader::MimeType mime_type_ =
|
||||
AuctionDownloader::MimeType::kJavascript;
|
||||
|
||||
std::unique_ptr<base::RunLoop> run_loop_;
|
||||
std::unique_ptr<std::string> body_;
|
||||
base::Optional<std::string> error_;
|
||||
|
||||
network::TestURLLoaderFactory url_loader_factory_;
|
||||
};
|
||||
@@ -79,6 +88,9 @@ TEST_F(AuctionDownloaderTest, NetworkError) {
|
||||
url_loader_factory_.AddResponse(url_, nullptr /* head */, kAsciiResponseBody,
|
||||
status);
|
||||
EXPECT_FALSE(RunRequest());
|
||||
EXPECT_EQ(
|
||||
"Failed to load https://url.test/script.js error = net::ERR_FAILED.",
|
||||
last_error_msg());
|
||||
}
|
||||
|
||||
// HTTP 404 responses are trested as failures.
|
||||
@@ -88,6 +100,9 @@ TEST_F(AuctionDownloaderTest, HttpError) {
|
||||
AddResponse(&url_loader_factory_, url_, kJavascriptMimeType, kUtf8Charset,
|
||||
kAsciiResponseBody, kAllowFledgeHeader, net::HTTP_NOT_FOUND);
|
||||
EXPECT_FALSE(RunRequest());
|
||||
EXPECT_EQ(
|
||||
"Failed to load https://url.test/script.js HTTP status = 404 Not Found.",
|
||||
last_error_msg());
|
||||
}
|
||||
|
||||
TEST_F(AuctionDownloaderTest, AllowFledge) {
|
||||
@@ -102,26 +117,50 @@ TEST_F(AuctionDownloaderTest, AllowFledge) {
|
||||
AddResponse(&url_loader_factory_, url_, kJavascriptMimeType, kUtf8Charset,
|
||||
kAsciiResponseBody, "X-Allow-FLEDGE: false");
|
||||
EXPECT_FALSE(RunRequest());
|
||||
EXPECT_EQ(
|
||||
"Rejecting load of https://url.test/script.js due to lack of "
|
||||
"X-Allow-FLEDGE: true.",
|
||||
last_error_msg());
|
||||
|
||||
AddResponse(&url_loader_factory_, url_, kJavascriptMimeType, kUtf8Charset,
|
||||
kAsciiResponseBody, "X-Allow-FLEDGE: sometimes");
|
||||
EXPECT_FALSE(RunRequest());
|
||||
EXPECT_EQ(
|
||||
"Rejecting load of https://url.test/script.js due to lack of "
|
||||
"X-Allow-FLEDGE: true.",
|
||||
last_error_msg());
|
||||
|
||||
AddResponse(&url_loader_factory_, url_, kJavascriptMimeType, kUtf8Charset,
|
||||
kAsciiResponseBody, "X-Allow-FLEDGE: ");
|
||||
EXPECT_FALSE(RunRequest());
|
||||
EXPECT_EQ(
|
||||
"Rejecting load of https://url.test/script.js due to lack of "
|
||||
"X-Allow-FLEDGE: true.",
|
||||
last_error_msg());
|
||||
|
||||
AddResponse(&url_loader_factory_, url_, kJavascriptMimeType, kUtf8Charset,
|
||||
kAsciiResponseBody, "X-Allow-Hats: true");
|
||||
EXPECT_FALSE(RunRequest());
|
||||
EXPECT_EQ(
|
||||
"Rejecting load of https://url.test/script.js due to lack of "
|
||||
"X-Allow-FLEDGE: true.",
|
||||
last_error_msg());
|
||||
|
||||
AddResponse(&url_loader_factory_, url_, kJavascriptMimeType, kUtf8Charset,
|
||||
kAsciiResponseBody, "");
|
||||
EXPECT_FALSE(RunRequest());
|
||||
EXPECT_EQ(
|
||||
"Rejecting load of https://url.test/script.js due to lack of "
|
||||
"X-Allow-FLEDGE: true.",
|
||||
last_error_msg());
|
||||
|
||||
AddResponse(&url_loader_factory_, url_, kJavascriptMimeType, kUtf8Charset,
|
||||
kAsciiResponseBody, base::nullopt);
|
||||
EXPECT_FALSE(RunRequest());
|
||||
EXPECT_EQ(
|
||||
"Rejecting load of https://url.test/script.js due to lack of "
|
||||
"X-Allow-FLEDGE: true.",
|
||||
last_error_msg());
|
||||
}
|
||||
|
||||
// Redirect responses are treated as failures.
|
||||
@@ -140,6 +179,8 @@ TEST_F(AuctionDownloaderTest, Redirect) {
|
||||
kAsciiResponseBody, kAllowFledgeHeader, net::HTTP_OK,
|
||||
std::move(redirects));
|
||||
EXPECT_FALSE(RunRequest());
|
||||
EXPECT_EQ("Unexpected redirect on https://url.test/script.js.",
|
||||
last_error_msg());
|
||||
}
|
||||
|
||||
TEST_F(AuctionDownloaderTest, Success) {
|
||||
@@ -156,40 +197,72 @@ TEST_F(AuctionDownloaderTest, MimeType) {
|
||||
AddResponse(&url_loader_factory_, url_, kJsonMimeType, kUtf8Charset,
|
||||
kAsciiResponseBody);
|
||||
EXPECT_FALSE(RunRequest());
|
||||
EXPECT_EQ(
|
||||
"Rejecting load of https://url.test/script.js due to unexpected MIME "
|
||||
"type.",
|
||||
last_error_msg());
|
||||
|
||||
// Javascript request, no response type.
|
||||
AddResponse(&url_loader_factory_, url_, base::nullopt, kUtf8Charset,
|
||||
kAsciiResponseBody);
|
||||
EXPECT_FALSE(RunRequest());
|
||||
EXPECT_EQ(
|
||||
"Rejecting load of https://url.test/script.js due to unexpected MIME "
|
||||
"type.",
|
||||
last_error_msg());
|
||||
|
||||
// Javascript request, empty response type.
|
||||
AddResponse(&url_loader_factory_, url_, "", kUtf8Charset, kAsciiResponseBody);
|
||||
EXPECT_FALSE(RunRequest());
|
||||
EXPECT_EQ(
|
||||
"Rejecting load of https://url.test/script.js due to unexpected MIME "
|
||||
"type.",
|
||||
last_error_msg());
|
||||
|
||||
// Javascript request, unknown response type.
|
||||
AddResponse(&url_loader_factory_, url_, "blobfish", kUtf8Charset,
|
||||
kAsciiResponseBody);
|
||||
EXPECT_FALSE(RunRequest());
|
||||
EXPECT_EQ(
|
||||
"Rejecting load of https://url.test/script.js due to unexpected MIME "
|
||||
"type.",
|
||||
last_error_msg());
|
||||
|
||||
// JSON request, Javascript response type.
|
||||
mime_type_ = AuctionDownloader::MimeType::kJson;
|
||||
AddResponse(&url_loader_factory_, url_, kJavascriptMimeType, kUtf8Charset,
|
||||
kAsciiResponseBody);
|
||||
EXPECT_FALSE(RunRequest());
|
||||
EXPECT_EQ(
|
||||
"Rejecting load of https://url.test/script.js due to unexpected MIME "
|
||||
"type.",
|
||||
last_error_msg());
|
||||
|
||||
// JSON request, no response type.
|
||||
AddResponse(&url_loader_factory_, url_, base::nullopt, kUtf8Charset,
|
||||
kAsciiResponseBody);
|
||||
EXPECT_FALSE(RunRequest());
|
||||
EXPECT_EQ(
|
||||
"Rejecting load of https://url.test/script.js due to unexpected MIME "
|
||||
"type.",
|
||||
last_error_msg());
|
||||
|
||||
// JSON request, empty response type.
|
||||
AddResponse(&url_loader_factory_, url_, "", kUtf8Charset, kAsciiResponseBody);
|
||||
EXPECT_FALSE(RunRequest());
|
||||
EXPECT_EQ(
|
||||
"Rejecting load of https://url.test/script.js due to unexpected MIME "
|
||||
"type.",
|
||||
last_error_msg());
|
||||
|
||||
// JSON request, unknown response type.
|
||||
AddResponse(&url_loader_factory_, url_, "blobfish", kUtf8Charset,
|
||||
kAsciiResponseBody);
|
||||
EXPECT_FALSE(RunRequest());
|
||||
EXPECT_EQ(
|
||||
"Rejecting load of https://url.test/script.js due to unexpected MIME "
|
||||
"type.",
|
||||
last_error_msg());
|
||||
|
||||
// JSON request, JSON response type.
|
||||
mime_type_ = AuctionDownloader::MimeType::kJson;
|
||||
@@ -241,6 +314,10 @@ TEST_F(AuctionDownloaderTest, MimeTypeVariants) {
|
||||
AddResponse(&url_loader_factory_, url_, javascript_type, kUtf8Charset,
|
||||
kAsciiResponseBody);
|
||||
EXPECT_FALSE(RunRequest());
|
||||
EXPECT_EQ(
|
||||
"Rejecting load of https://url.test/script.js due to unexpected MIME "
|
||||
"type.",
|
||||
last_error_msg());
|
||||
}
|
||||
|
||||
for (const char* json_type : kJsonMimeTypes) {
|
||||
@@ -248,6 +325,10 @@ TEST_F(AuctionDownloaderTest, MimeTypeVariants) {
|
||||
AddResponse(&url_loader_factory_, url_, json_type, kUtf8Charset,
|
||||
kAsciiResponseBody);
|
||||
EXPECT_FALSE(RunRequest());
|
||||
EXPECT_EQ(
|
||||
"Rejecting load of https://url.test/script.js due to unexpected MIME "
|
||||
"type.",
|
||||
last_error_msg());
|
||||
|
||||
mime_type_ = AuctionDownloader::MimeType::kJson;
|
||||
AddResponse(&url_loader_factory_, url_, json_type, kUtf8Charset,
|
||||
@@ -259,18 +340,31 @@ TEST_F(AuctionDownloaderTest, MimeTypeVariants) {
|
||||
}
|
||||
|
||||
TEST_F(AuctionDownloaderTest, Charset) {
|
||||
mime_type_ = AuctionDownloader::MimeType::kJson;
|
||||
// Unknown/unsupported charsets should result in failure.
|
||||
AddResponse(&url_loader_factory_, url_, kJsonMimeType, "fred",
|
||||
kAsciiResponseBody);
|
||||
EXPECT_FALSE(RunRequest());
|
||||
EXPECT_EQ(
|
||||
"Rejecting load of https://url.test/script.js due to unexpected charset.",
|
||||
last_error_msg());
|
||||
|
||||
AddResponse(&url_loader_factory_, url_, kJsonMimeType, "iso-8859-1",
|
||||
kAsciiResponseBody);
|
||||
EXPECT_FALSE(RunRequest());
|
||||
EXPECT_EQ(
|
||||
"Rejecting load of https://url.test/script.js due to unexpected charset.",
|
||||
last_error_msg());
|
||||
|
||||
// ASCII charset should restrict response bodies to ASCII characters.
|
||||
mime_type_ = AuctionDownloader::MimeType::kJavascript;
|
||||
AddResponse(&url_loader_factory_, url_, kJavascriptMimeType, kAsciiCharset,
|
||||
kUtf8ResponseBody);
|
||||
EXPECT_FALSE(RunRequest());
|
||||
EXPECT_EQ(
|
||||
"Rejecting load of https://url.test/script.js due to unexpected charset.",
|
||||
last_error_msg());
|
||||
|
||||
AddResponse(&url_loader_factory_, url_, kJavascriptMimeType, kAsciiCharset,
|
||||
kAsciiResponseBody);
|
||||
std::unique_ptr<std::string> body = RunRequest();
|
||||
@@ -279,9 +373,16 @@ TEST_F(AuctionDownloaderTest, Charset) {
|
||||
AddResponse(&url_loader_factory_, url_, kJavascriptMimeType, kAsciiCharset,
|
||||
kUtf8ResponseBody);
|
||||
EXPECT_FALSE(RunRequest());
|
||||
EXPECT_EQ(
|
||||
"Rejecting load of https://url.test/script.js due to unexpected charset.",
|
||||
last_error_msg());
|
||||
|
||||
AddResponse(&url_loader_factory_, url_, kJavascriptMimeType, kAsciiCharset,
|
||||
kNonUtf8ResponseBody);
|
||||
EXPECT_FALSE(RunRequest());
|
||||
EXPECT_EQ(
|
||||
"Rejecting load of https://url.test/script.js due to unexpected charset.",
|
||||
last_error_msg());
|
||||
|
||||
// UTF-8 charset should restrict response bodies to valid UTF-8 characters.
|
||||
AddResponse(&url_loader_factory_, url_, kJavascriptMimeType, kUtf8Charset,
|
||||
@@ -297,6 +398,9 @@ TEST_F(AuctionDownloaderTest, Charset) {
|
||||
AddResponse(&url_loader_factory_, url_, kJavascriptMimeType, kUtf8Charset,
|
||||
kNonUtf8ResponseBody);
|
||||
EXPECT_FALSE(RunRequest());
|
||||
EXPECT_EQ(
|
||||
"Rejecting load of https://url.test/script.js due to unexpected charset.",
|
||||
last_error_msg());
|
||||
|
||||
// Null charset should act like UTF-8.
|
||||
AddResponse(&url_loader_factory_, url_, kJavascriptMimeType, base::nullopt,
|
||||
@@ -312,6 +416,9 @@ TEST_F(AuctionDownloaderTest, Charset) {
|
||||
AddResponse(&url_loader_factory_, url_, kJavascriptMimeType, base::nullopt,
|
||||
kNonUtf8ResponseBody);
|
||||
EXPECT_FALSE(RunRequest());
|
||||
EXPECT_EQ(
|
||||
"Rejecting load of https://url.test/script.js due to unexpected charset.",
|
||||
last_error_msg());
|
||||
|
||||
// Empty charset should act like UTF-8.
|
||||
AddResponse(&url_loader_factory_, url_, kJavascriptMimeType, "",
|
||||
@@ -327,6 +434,9 @@ TEST_F(AuctionDownloaderTest, Charset) {
|
||||
AddResponse(&url_loader_factory_, url_, kJavascriptMimeType, "",
|
||||
kNonUtf8ResponseBody);
|
||||
EXPECT_FALSE(RunRequest());
|
||||
EXPECT_EQ(
|
||||
"Rejecting load of https://url.test/script.js due to unexpected charset.",
|
||||
last_error_msg());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
@@ -80,7 +80,7 @@ void AuctionRunner::StartBidding() {
|
||||
base::BindOnce(&AuctionRunner::OnTrustedSignalsLoaded,
|
||||
base::Unretained(this), bid_state));
|
||||
} else {
|
||||
OnTrustedSignalsLoaded(bid_state, false);
|
||||
OnTrustedSignalsLoaded(bid_state, false, base::nullopt);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,9 +92,16 @@ void AuctionRunner::StartBidding() {
|
||||
base::Unretained(this)));
|
||||
}
|
||||
|
||||
void AuctionRunner::OnBidderScriptLoaded(BidState* state, bool load_result) {
|
||||
void AuctionRunner::OnBidderScriptLoaded(
|
||||
BidState* state,
|
||||
bool load_result,
|
||||
base::Optional<std::string> error_msg) {
|
||||
DCHECK(!state->bidder_script_loaded);
|
||||
DCHECK(!state->failed);
|
||||
|
||||
if (error_msg.has_value())
|
||||
errors_.push_back(std::move(error_msg).value());
|
||||
|
||||
state->bidder_script_loaded = true;
|
||||
if (!load_result) {
|
||||
state->failed = true;
|
||||
@@ -108,8 +115,15 @@ void AuctionRunner::OnBidderScriptLoaded(BidState* state, bool load_result) {
|
||||
MaybeRunBid(state);
|
||||
}
|
||||
|
||||
void AuctionRunner::OnTrustedSignalsLoaded(BidState* state, bool load_result) {
|
||||
void AuctionRunner::OnTrustedSignalsLoaded(
|
||||
BidState* state,
|
||||
bool load_result,
|
||||
base::Optional<std::string> error_msg) {
|
||||
DCHECK(!state->trusted_signals_loaded);
|
||||
|
||||
if (error_msg.has_value())
|
||||
errors_.push_back(std::move(error_msg).value());
|
||||
|
||||
state->trusted_signals_loaded = true;
|
||||
if (!load_result)
|
||||
state->trusted_bidding_signals.reset();
|
||||
@@ -133,10 +147,17 @@ void AuctionRunner::RunBid(BidState* state) {
|
||||
base::TimeTicks start = base::TimeTicks::Now();
|
||||
state->bid_result =
|
||||
state->bidder_worklet->GenerateBid(state->trusted_bidding_signals.get());
|
||||
if (state->bid_result.error_msg.has_value())
|
||||
errors_.push_back(std::move(state->bid_result.error_msg).value());
|
||||
state->bid_duration = base::TimeTicks::Now() - start;
|
||||
}
|
||||
|
||||
void AuctionRunner::OnSellerWorkletLoaded(bool load_result) {
|
||||
void AuctionRunner::OnSellerWorkletLoaded(
|
||||
bool load_result,
|
||||
base::Optional<std::string> error_msg) {
|
||||
if (error_msg.has_value())
|
||||
errors_.push_back(std::move(error_msg).value());
|
||||
|
||||
if (load_result) {
|
||||
seller_loaded_ = true;
|
||||
if (ReadyToScore())
|
||||
@@ -177,10 +198,13 @@ void AuctionRunner::ScoreOne() {
|
||||
}
|
||||
|
||||
SellerWorklet::ScoreResult AuctionRunner::ScoreBid(const BidState* state) {
|
||||
return seller_worklet_->ScoreAd(
|
||||
SellerWorklet::ScoreResult result = seller_worklet_->ScoreAd(
|
||||
state->bid_result.ad, state->bid_result.bid, *auction_config_,
|
||||
browser_signals_->top_frame_origin.host(), state->bidder->group->owner,
|
||||
AdRenderFingerprint(state), state->bid_duration);
|
||||
if (result.error_msg.has_value())
|
||||
errors_.push_back(std::move(result.error_msg).value());
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string AuctionRunner::AdRenderFingerprint(const BidState* state) {
|
||||
@@ -225,26 +249,32 @@ void AuctionRunner::CompleteAuction() {
|
||||
|
||||
SellerWorklet::Report AuctionRunner::ReportSellerResult(
|
||||
const BidState* best_bid) {
|
||||
return seller_worklet_->ReportResult(
|
||||
SellerWorklet::Report result = seller_worklet_->ReportResult(
|
||||
*auction_config_, browser_signals_->top_frame_origin.host(),
|
||||
best_bid->bidder->group->owner, best_bid->bid_result.render_url,
|
||||
AdRenderFingerprint(best_bid), best_bid->bid_result.bid,
|
||||
best_bid->score_result.score);
|
||||
if (result.error_msg.has_value())
|
||||
errors_.push_back(std::move(result.error_msg).value());
|
||||
return result;
|
||||
}
|
||||
|
||||
BidderWorklet::ReportWinResult AuctionRunner::ReportBidWin(
|
||||
const BidState* best_bid,
|
||||
const SellerWorklet::Report& seller_report) {
|
||||
return best_bid->bidder_worklet->ReportWin(
|
||||
BidderWorklet::ReportWinResult result = best_bid->bidder_worklet->ReportWin(
|
||||
seller_report.signals_for_winner, best_bid->bid_result.render_url,
|
||||
AdRenderFingerprint(best_bid), best_bid->bid_result.bid);
|
||||
if (result.error_msg.has_value())
|
||||
errors_.push_back(std::move(result.error_msg).value());
|
||||
return result;
|
||||
}
|
||||
|
||||
void AuctionRunner::FailAuction() {
|
||||
std::move(callback_).Run(
|
||||
GURL(), url::Origin(), std::string(),
|
||||
mojom::WinningBidderReport::New(false /* success */, GURL()),
|
||||
mojom::SellerReport::New(false /* success */, "", GURL()));
|
||||
mojom::SellerReport::New(false /* success */, "", GURL()), errors_);
|
||||
delete this;
|
||||
}
|
||||
|
||||
@@ -259,7 +289,8 @@ void AuctionRunner::ReportSuccess(
|
||||
bidder_report.report_url),
|
||||
mojom::SellerReport::New(seller_report.success,
|
||||
seller_report.signals_for_winner,
|
||||
seller_report.report_url));
|
||||
seller_report.report_url),
|
||||
errors_);
|
||||
delete this;
|
||||
}
|
||||
|
||||
|
@@ -5,6 +5,7 @@
|
||||
#ifndef CONTENT_SERVICES_AUCTION_WORKLET_AUCTION_RUNNER_H_
|
||||
#define CONTENT_SERVICES_AUCTION_WORKLET_AUCTION_RUNNER_H_
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "base/callback_forward.h"
|
||||
@@ -85,14 +86,19 @@ class AuctionRunner {
|
||||
~AuctionRunner();
|
||||
|
||||
void StartBidding();
|
||||
void OnBidderScriptLoaded(BidState* state, bool load_result);
|
||||
void OnTrustedSignalsLoaded(BidState* state, bool load_result);
|
||||
void OnBidderScriptLoaded(BidState* state,
|
||||
bool load_result,
|
||||
base::Optional<std::string> error_msg);
|
||||
void OnTrustedSignalsLoaded(BidState* state,
|
||||
bool load_result,
|
||||
base::Optional<std::string> error_msg);
|
||||
void MaybeRunBid(BidState* state);
|
||||
void RunBid(BidState* state);
|
||||
|
||||
// True if all bid results and the seller script load are complete.
|
||||
bool ReadyToScore() const { return outstanding_bids_ == 0 && seller_loaded_; }
|
||||
void OnSellerWorkletLoaded(bool load_result);
|
||||
void OnSellerWorkletLoaded(bool load_result,
|
||||
base::Optional<std::string> error_msg);
|
||||
|
||||
// Lets the seller score a single outstanding bid, if any, and then either
|
||||
// re-queues itself on event loop if there is more to check, or proceeds to
|
||||
@@ -145,6 +151,9 @@ class AuctionRunner {
|
||||
// that can be done.
|
||||
bool seller_loaded_ = false;
|
||||
size_t seller_considering_ = 0;
|
||||
|
||||
// All errors reported by worklets thus far.
|
||||
std::vector<std::string> errors_;
|
||||
};
|
||||
|
||||
} // namespace auction_worklet
|
||||
|
@@ -14,6 +14,7 @@
|
||||
#include "content/services/auction_worklet/worklet_test_util.h"
|
||||
#include "net/http/http_status_code.h"
|
||||
#include "services/network/test/test_url_loader_factory.h"
|
||||
#include "testing/gmock/include/gmock/gmock-matchers.h"
|
||||
#include "testing/gtest/include/gtest/gtest.h"
|
||||
|
||||
namespace auction_worklet {
|
||||
@@ -205,6 +206,7 @@ class AuctionRunnerTest : public testing::Test {
|
||||
std::string interest_group_name;
|
||||
mojom::WinningBidderReportPtr bidder_report;
|
||||
mojom::SellerReportPtr seller_report;
|
||||
std::vector<std::string> errors;
|
||||
};
|
||||
|
||||
Result RunAuctionAndWait(const GURL& seller_decision_logic_url,
|
||||
@@ -238,12 +240,14 @@ class AuctionRunnerTest : public testing::Test {
|
||||
const url::Origin& interest_group_owner,
|
||||
const std::string& interest_group_name,
|
||||
mojom::WinningBidderReportPtr bid_report,
|
||||
mojom::SellerReportPtr seller_report) {
|
||||
mojom::SellerReportPtr seller_report,
|
||||
const std::vector<std::string>& errors) {
|
||||
result.ad_url = ad_url;
|
||||
result.interest_group_owner = interest_group_owner;
|
||||
result.interest_group_name = interest_group_name;
|
||||
result.bidder_report = std::move(bid_report);
|
||||
result.seller_report = std::move(seller_report);
|
||||
result.errors = errors;
|
||||
run_loop.Quit();
|
||||
}));
|
||||
run_loop.Run();
|
||||
@@ -346,6 +350,7 @@ TEST_F(AuctionRunnerTest, Basic) {
|
||||
EXPECT_TRUE(res.bidder_report->report_requested);
|
||||
EXPECT_EQ("https://buyer-reporting.example.com/",
|
||||
res.bidder_report->report_url.spec());
|
||||
EXPECT_THAT(res.errors, testing::ElementsAre());
|
||||
}
|
||||
|
||||
// An auction where one bid is successful, another's script 404s.
|
||||
@@ -381,6 +386,10 @@ TEST_F(AuctionRunnerTest, OneBidOne404) {
|
||||
EXPECT_TRUE(res.bidder_report->report_requested);
|
||||
EXPECT_EQ("https://buyer-reporting.example.com/",
|
||||
res.bidder_report->report_url.spec());
|
||||
EXPECT_THAT(
|
||||
res.errors,
|
||||
testing::ElementsAre("Failed to load https://anotheradthing.com/bids.js "
|
||||
"HTTP status = 404 Not Found."));
|
||||
}
|
||||
|
||||
// An auction where one bid is successful, another's script does not provide a
|
||||
@@ -419,6 +428,9 @@ TEST_F(AuctionRunnerTest, OneBidOneNotMade) {
|
||||
EXPECT_TRUE(res.bidder_report->report_requested);
|
||||
EXPECT_EQ("https://buyer-reporting.example.com/",
|
||||
res.bidder_report->report_url.spec());
|
||||
EXPECT_THAT(res.errors,
|
||||
testing::ElementsAre("https://anotheradthing.com/bids.js "
|
||||
"`generateBid` is not a function."));
|
||||
}
|
||||
|
||||
// An auction where no bidding scripts load successfully.
|
||||
@@ -444,6 +456,12 @@ TEST_F(AuctionRunnerTest, NoBids) {
|
||||
EXPECT_EQ("", res.seller_report->signals_for_winner_json);
|
||||
EXPECT_FALSE(res.bidder_report->report_requested);
|
||||
EXPECT_TRUE(res.bidder_report->report_url.is_empty());
|
||||
EXPECT_THAT(
|
||||
res.errors,
|
||||
testing::ElementsAre("Failed to load https://adplatform.com/offers.js "
|
||||
"HTTP status = 404 Not Found.",
|
||||
"Failed to load https://anotheradthing.com/bids.js "
|
||||
"HTTP status = 404 Not Found."));
|
||||
}
|
||||
|
||||
// An auction where none of the bidding scripts has a valid bidding function.
|
||||
@@ -470,6 +488,12 @@ TEST_F(AuctionRunnerTest, NoBidMadeByScript) {
|
||||
EXPECT_EQ("", res.seller_report->signals_for_winner_json);
|
||||
EXPECT_FALSE(res.bidder_report->report_requested);
|
||||
EXPECT_TRUE(res.bidder_report->report_url.is_empty());
|
||||
EXPECT_THAT(
|
||||
res.errors,
|
||||
testing::ElementsAre(
|
||||
"https://adplatform.com/offers.js `generateBid` is not a function.",
|
||||
"https://anotheradthing.com/bids.js `generateBid` is not a "
|
||||
"function."));
|
||||
}
|
||||
|
||||
// An auction where the seller script doesn't have a scoring function.
|
||||
@@ -503,6 +527,11 @@ TEST_F(AuctionRunnerTest, SellerRejectsAll) {
|
||||
EXPECT_EQ("", res.seller_report->signals_for_winner_json);
|
||||
EXPECT_FALSE(res.bidder_report->report_requested);
|
||||
EXPECT_TRUE(res.bidder_report->report_url.is_empty());
|
||||
EXPECT_THAT(res.errors,
|
||||
testing::ElementsAre("https://adstuff.publisher1.com/auction.js "
|
||||
"`scoreAd` is not a function.",
|
||||
"https://adstuff.publisher1.com/auction.js "
|
||||
"`scoreAd` is not a function."));
|
||||
}
|
||||
|
||||
// An auction where seller rejects one bid when scoring.
|
||||
@@ -560,6 +589,10 @@ TEST_F(AuctionRunnerTest, NoSellerScript) {
|
||||
EXPECT_TRUE(res.bidder_report->report_url.is_empty());
|
||||
|
||||
EXPECT_EQ(0, url_loader_factory_.NumPending());
|
||||
EXPECT_THAT(res.errors,
|
||||
testing::ElementsAre(
|
||||
"Failed to load https://adstuff.publisher1.com/auction.js "
|
||||
"HTTP status = 404 Not Found."));
|
||||
}
|
||||
|
||||
// An auction where bidders don't requested trusted bidding signals.
|
||||
@@ -604,6 +637,7 @@ TEST_F(AuctionRunnerTest, NoTrustedBiddingSignals) {
|
||||
EXPECT_TRUE(res.bidder_report->report_requested);
|
||||
EXPECT_EQ("https://buyer-reporting.example.com/",
|
||||
res.bidder_report->report_url.spec());
|
||||
EXPECT_THAT(res.errors, testing::ElementsAre());
|
||||
}
|
||||
|
||||
// An auction where trusted bidding signals are requested, but the fetch 404s.
|
||||
@@ -640,6 +674,15 @@ TEST_F(AuctionRunnerTest, TrustedBiddingSignals404) {
|
||||
EXPECT_TRUE(res.bidder_report->report_requested);
|
||||
EXPECT_EQ("https://buyer-reporting.example.com/",
|
||||
res.bidder_report->report_url.spec());
|
||||
EXPECT_THAT(res.errors,
|
||||
testing::ElementsAre("Failed to load "
|
||||
"https://trustedsignaller.org/"
|
||||
"signals?hostname=publisher1.com&keys=k1,k2 "
|
||||
"HTTP status = 404 Not Found.",
|
||||
"Failed to load "
|
||||
"https://trustedsignaller.org/"
|
||||
"signals?hostname=publisher1.com&keys=l1,l2 "
|
||||
"HTTP status = 404 Not Found."));
|
||||
}
|
||||
|
||||
// A successful auction where seller reporting worklet doesn't set a URL.
|
||||
@@ -678,6 +721,7 @@ TEST_F(AuctionRunnerTest, NoReportResultUrl) {
|
||||
EXPECT_TRUE(res.bidder_report->report_requested);
|
||||
EXPECT_EQ("https://buyer-reporting.example.com/",
|
||||
res.bidder_report->report_url.spec());
|
||||
EXPECT_THAT(res.errors, testing::ElementsAre());
|
||||
}
|
||||
|
||||
// A successful auction where bidder reporting worklet doesn't set a URL.
|
||||
@@ -717,6 +761,7 @@ TEST_F(AuctionRunnerTest, NoReportWinUrl) {
|
||||
res.seller_report->signals_for_winner_json);
|
||||
EXPECT_FALSE(res.bidder_report->report_requested);
|
||||
EXPECT_TRUE(res.bidder_report->report_url.is_empty());
|
||||
EXPECT_THAT(res.errors, testing::ElementsAre());
|
||||
}
|
||||
|
||||
// A successful auction where neither reporting worklets sets a URL.
|
||||
@@ -756,6 +801,7 @@ TEST_F(AuctionRunnerTest, NeitherReportUrl) {
|
||||
res.seller_report->signals_for_winner_json);
|
||||
EXPECT_FALSE(res.bidder_report->report_requested);
|
||||
EXPECT_TRUE(res.bidder_report->report_url.is_empty());
|
||||
EXPECT_THAT(res.errors, testing::ElementsAre());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
@@ -10,6 +10,7 @@
|
||||
#include "base/memory/ref_counted.h"
|
||||
#include "base/memory/weak_ptr.h"
|
||||
#include "base/sequenced_task_runner.h"
|
||||
#include "base/strings/strcat.h"
|
||||
#include "base/strings/string_number_conversions.h"
|
||||
#include "base/strings/string_util.h"
|
||||
#include "base/synchronization/lock.h"
|
||||
@@ -275,7 +276,8 @@ bool AuctionV8Helper::ExtractJson(v8::Local<v8::Context> context,
|
||||
|
||||
v8::MaybeLocal<v8::UnboundScript> AuctionV8Helper::Compile(
|
||||
const std::string& src,
|
||||
const GURL& src_url) {
|
||||
const GURL& src_url,
|
||||
base::Optional<std::string>& error_out) {
|
||||
v8::Isolate* v8_isolate = isolate();
|
||||
|
||||
v8::MaybeLocal<v8::String> src_string = CreateUtf8String(src);
|
||||
@@ -291,8 +293,10 @@ v8::MaybeLocal<v8::UnboundScript> AuctionV8Helper::Compile(
|
||||
auto result = v8::ScriptCompiler::CompileUnboundScript(
|
||||
v8_isolate, &script_source, v8::ScriptCompiler::kNoCompileOptions,
|
||||
v8::ScriptCompiler::NoCacheReason::kNoCacheNoReason);
|
||||
if (try_catch.HasCaught())
|
||||
PrintMessage(v8_isolate->GetCurrentContext(), try_catch.Message());
|
||||
if (try_catch.HasCaught()) {
|
||||
error_out = FormatExceptionMessage(v8_isolate->GetCurrentContext(),
|
||||
try_catch.Message());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -300,7 +304,8 @@ v8::MaybeLocal<v8::Value> AuctionV8Helper::RunScript(
|
||||
v8::Local<v8::Context> context,
|
||||
v8::Local<v8::UnboundScript> script,
|
||||
base::StringPiece script_name,
|
||||
base::span<v8::Local<v8::Value>> args) {
|
||||
base::span<v8::Local<v8::Value>> args,
|
||||
base::Optional<std::string>& error_out) {
|
||||
DCHECK_EQ(isolate(), context->GetIsolate());
|
||||
|
||||
v8::Local<v8::String> v8_script_name;
|
||||
@@ -313,8 +318,15 @@ v8::MaybeLocal<v8::Value> AuctionV8Helper::RunScript(
|
||||
v8::TryCatch try_catch(isolate());
|
||||
ScriptTimeoutHelper timeout_helper(isolate(), script_timeout_);
|
||||
auto result = local_script->Run(context);
|
||||
|
||||
if (try_catch.HasTerminated()) {
|
||||
error_out = base::StrCat({FormatValue(isolate(), script->GetScriptName()),
|
||||
" top-level execution timed out."});
|
||||
return v8::MaybeLocal<v8::Value>();
|
||||
}
|
||||
|
||||
if (try_catch.HasCaught()) {
|
||||
PrintMessage(context, try_catch.Message());
|
||||
error_out = FormatExceptionMessage(context, try_catch.Message());
|
||||
return v8::MaybeLocal<v8::Value>();
|
||||
}
|
||||
|
||||
@@ -322,35 +334,47 @@ v8::MaybeLocal<v8::Value> AuctionV8Helper::RunScript(
|
||||
return v8::MaybeLocal<v8::Value>();
|
||||
|
||||
v8::Local<v8::Value> function;
|
||||
if (!context->Global()->Get(context, v8_script_name).ToLocal(&function))
|
||||
if (!context->Global()->Get(context, v8_script_name).ToLocal(&function)) {
|
||||
error_out = base::StrCat({FormatValue(isolate(), script->GetScriptName()),
|
||||
" function `", script_name, "` not found."});
|
||||
return v8::MaybeLocal<v8::Value>();
|
||||
}
|
||||
|
||||
if (!function->IsFunction())
|
||||
if (!function->IsFunction()) {
|
||||
error_out = base::StrCat({FormatValue(isolate(), script->GetScriptName()),
|
||||
" `", script_name, "` is not a function."});
|
||||
return v8::MaybeLocal<v8::Value>();
|
||||
}
|
||||
|
||||
v8::MaybeLocal<v8::Value> func_result = v8::Function::Cast(*function)->Call(
|
||||
context, context->Global(), args.size(), args.data());
|
||||
if (try_catch.HasTerminated()) {
|
||||
error_out = base::StrCat({FormatValue(isolate(), script->GetScriptName()),
|
||||
" execution of `", script_name, "` timed out."});
|
||||
return v8::MaybeLocal<v8::Value>();
|
||||
}
|
||||
if (try_catch.HasCaught()) {
|
||||
PrintMessage(context, try_catch.Message());
|
||||
error_out = FormatExceptionMessage(context, try_catch.Message());
|
||||
return v8::MaybeLocal<v8::Value>();
|
||||
}
|
||||
return func_result;
|
||||
}
|
||||
|
||||
// static
|
||||
void AuctionV8Helper::PrintMessage(v8::Local<v8::Context> context,
|
||||
v8::Local<v8::Message> message) {
|
||||
std::string AuctionV8Helper::FormatExceptionMessage(
|
||||
v8::Local<v8::Context> context,
|
||||
v8::Local<v8::Message> message) {
|
||||
if (message.IsEmpty()) {
|
||||
LOG(ERROR) << "Unknown exception";
|
||||
return "Unknown exception.";
|
||||
} else {
|
||||
v8::Isolate* isolate = message->GetIsolate();
|
||||
int line_num;
|
||||
LOG(ERROR) << FormatValue(isolate, message->GetScriptResourceName())
|
||||
<< (!context.IsEmpty() &&
|
||||
message->GetLineNumber(context).To(&line_num)
|
||||
? std::string(":") + base::NumberToString(line_num)
|
||||
: std::string())
|
||||
<< " " << FormatValue(isolate, message->Get());
|
||||
return base::StrCat(
|
||||
{FormatValue(isolate, message->GetScriptResourceName()),
|
||||
!context.IsEmpty() && message->GetLineNumber(context).To(&line_num)
|
||||
? std::string(":") + base::NumberToString(line_num)
|
||||
: std::string(),
|
||||
" ", FormatValue(isolate, message->Get()), "."});
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -6,9 +6,11 @@
|
||||
#define CONTENT_SERVICES_AUCTION_WORKLET_AUCTION_V8_HELPER_H_
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "base/compiler_specific.h"
|
||||
#include "base/containers/span.h"
|
||||
#include "base/optional.h"
|
||||
#include "base/strings/string_piece.h"
|
||||
#include "base/time/time.h"
|
||||
#include "gin/public/isolate_holder.h"
|
||||
@@ -108,9 +110,12 @@ class AuctionV8Helper {
|
||||
std::string* out);
|
||||
|
||||
// Compiles the provided script. Despite not being bound to a context, there
|
||||
// still must be an active context for this method to be invoked.
|
||||
v8::MaybeLocal<v8::UnboundScript> Compile(const std::string& src,
|
||||
const GURL& src_url);
|
||||
// still must be an active context for this method to be invoked. In case of
|
||||
// an error, sets `error_out`.
|
||||
v8::MaybeLocal<v8::UnboundScript> Compile(
|
||||
const std::string& src,
|
||||
const GURL& src_url,
|
||||
base::Optional<std::string>& error_out);
|
||||
|
||||
// Binds a script and runs it in the passed in context, returning the result.
|
||||
// Note that the returned value could include references to objects or
|
||||
@@ -122,19 +127,21 @@ class AuctionV8Helper {
|
||||
//
|
||||
// Running this multiple times in the same context will re-load the entire
|
||||
// script file in the context, and then run the script again.
|
||||
v8::MaybeLocal<v8::Value> RunScript(
|
||||
v8::Local<v8::Context> context,
|
||||
v8::Local<v8::UnboundScript> script,
|
||||
base::StringPiece script_name,
|
||||
base::span<v8::Local<v8::Value>> args = {});
|
||||
//
|
||||
// In case of an error, sets `error_out`.
|
||||
v8::MaybeLocal<v8::Value> RunScript(v8::Local<v8::Context> context,
|
||||
v8::Local<v8::UnboundScript> script,
|
||||
base::StringPiece script_name,
|
||||
base::span<v8::Local<v8::Value>> args,
|
||||
base::Optional<std::string>& error_out);
|
||||
|
||||
void set_script_timeout_for_testing(base::TimeDelta script_timeout) {
|
||||
script_timeout_ = script_timeout;
|
||||
}
|
||||
|
||||
private:
|
||||
static void PrintMessage(v8::Local<v8::Context> context,
|
||||
v8::Local<v8::Message> message);
|
||||
static std::string FormatExceptionMessage(v8::Local<v8::Context> context,
|
||||
v8::Local<v8::Message> message);
|
||||
static std::string FormatValue(v8::Isolate* isolate,
|
||||
v8::Local<v8::Value> val);
|
||||
|
||||
|
@@ -6,13 +6,18 @@
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "base/optional.h"
|
||||
#include "base/test/task_environment.h"
|
||||
#include "base/time/time.h"
|
||||
#include "gin/converter.h"
|
||||
#include "testing/gmock/include/gmock/gmock-matchers.h"
|
||||
#include "testing/gtest/include/gtest/gtest.h"
|
||||
#include "url/gurl.h"
|
||||
#include "v8/include/v8.h"
|
||||
|
||||
using testing::HasSubstr;
|
||||
using testing::StartsWith;
|
||||
|
||||
namespace auction_worklet {
|
||||
|
||||
class AuctionV8HelperTest : public testing::Test {
|
||||
@@ -32,53 +37,77 @@ TEST_F(AuctionV8HelperTest, Basic) {
|
||||
v8::Local<v8::UnboundScript> script;
|
||||
{
|
||||
v8::Context::Scope ctx(helper_.scratch_context());
|
||||
ASSERT_TRUE(
|
||||
helper_
|
||||
.Compile("function foo() { return 1;}", GURL("https://foo.test/"))
|
||||
.ToLocal(&script));
|
||||
base::Optional<std::string> error_msg;
|
||||
ASSERT_TRUE(helper_
|
||||
.Compile("function foo() { return 1;}",
|
||||
GURL("https://foo.test/"), error_msg)
|
||||
.ToLocal(&script));
|
||||
EXPECT_FALSE(error_msg.has_value());
|
||||
}
|
||||
|
||||
for (v8::Local<v8::Context> context :
|
||||
{helper_.scratch_context(), helper_.CreateContext()}) {
|
||||
base::Optional<std::string> error_msg;
|
||||
v8::Context::Scope ctx(context);
|
||||
v8::Local<v8::Value> result;
|
||||
ASSERT_TRUE(helper_.RunScript(context, script, "foo").ToLocal(&result));
|
||||
ASSERT_TRUE(helper_
|
||||
.RunScript(context, script, "foo",
|
||||
base::span<v8::Local<v8::Value>>(), error_msg)
|
||||
.ToLocal(&result));
|
||||
int int_result = 0;
|
||||
ASSERT_TRUE(gin::ConvertFromV8(helper_.isolate(), result, &int_result));
|
||||
EXPECT_EQ(1, int_result);
|
||||
EXPECT_FALSE(error_msg.has_value());
|
||||
}
|
||||
}
|
||||
|
||||
// Check that timing out scripts works.
|
||||
TEST_F(AuctionV8HelperTest, Timeout) {
|
||||
const char* kHangingScripts[] = {
|
||||
struct HangingScript {
|
||||
const char* script;
|
||||
bool top_level_hangs;
|
||||
};
|
||||
|
||||
const HangingScript kHangingScripts[] = {
|
||||
// Script that times out when run. Its foo() method returns 1, but should
|
||||
// never be called.
|
||||
R"(function foo() { return 1;}
|
||||
while(1);)",
|
||||
{R"(function foo() { return 1;}
|
||||
while(1);)",
|
||||
true},
|
||||
|
||||
// Script that times out when foo() is called.
|
||||
"function foo() {while (1);}",
|
||||
{"function foo() {while (1);}", false},
|
||||
|
||||
// Script that times out when run and when foo is called.
|
||||
R"(function foo() {while (1);}
|
||||
while(1);)",
|
||||
};
|
||||
{R"(function foo() {while (1);}
|
||||
while(1);)",
|
||||
true}};
|
||||
|
||||
// Use a sorter timeout so test runs faster.
|
||||
const base::TimeDelta kScriptTimeout = base::TimeDelta::FromMilliseconds(20);
|
||||
helper_.set_script_timeout_for_testing(kScriptTimeout);
|
||||
|
||||
for (const char* hanging_script : kHangingScripts) {
|
||||
for (const HangingScript& hanging_script : kHangingScripts) {
|
||||
base::TimeTicks start_time = base::TimeTicks::Now();
|
||||
v8::Local<v8::Context> context = helper_.CreateContext();
|
||||
v8::Context::Scope context_scope(context);
|
||||
|
||||
v8::Local<v8::UnboundScript> script;
|
||||
ASSERT_TRUE(helper_.Compile(hanging_script, GURL("https://foo.test/"))
|
||||
base::Optional<std::string> error_msg;
|
||||
ASSERT_TRUE(helper_
|
||||
.Compile(hanging_script.script, GURL("https://foo.test/"),
|
||||
error_msg)
|
||||
.ToLocal(&script));
|
||||
EXPECT_FALSE(error_msg.has_value());
|
||||
|
||||
v8::MaybeLocal<v8::Value> result =
|
||||
helper_.RunScript(context, script, "foo");
|
||||
v8::MaybeLocal<v8::Value> result = helper_.RunScript(
|
||||
context, script, "foo", base::span<v8::Local<v8::Value>>(), error_msg);
|
||||
EXPECT_TRUE(result.IsEmpty());
|
||||
ASSERT_TRUE(error_msg.has_value());
|
||||
EXPECT_EQ(hanging_script.top_level_hangs
|
||||
? "https://foo.test/ top-level execution timed out."
|
||||
: "https://foo.test/ execution of `foo` timed out.",
|
||||
error_msg.value());
|
||||
|
||||
// Make sure at least `kScriptTimeout` has passed, allowing for some time
|
||||
// skew between change in base::TimeTicks::Now() and the timeout. This
|
||||
@@ -93,11 +122,18 @@ TEST_F(AuctionV8HelperTest, Timeout) {
|
||||
v8::Local<v8::Context> context = helper_.CreateContext();
|
||||
v8::Context::Scope context_scope(context);
|
||||
v8::Local<v8::UnboundScript> script;
|
||||
ASSERT_TRUE(
|
||||
helper_.Compile("function foo() { return 1;}", GURL("https://foo.test/"))
|
||||
.ToLocal(&script));
|
||||
base::Optional<std::string> error_msg;
|
||||
ASSERT_TRUE(helper_
|
||||
.Compile("function foo() { return 1;}",
|
||||
GURL("https://foo.test/"), error_msg)
|
||||
.ToLocal(&script));
|
||||
EXPECT_FALSE(error_msg.has_value());
|
||||
v8::Local<v8::Value> result;
|
||||
ASSERT_TRUE(helper_.RunScript(context, script, "foo").ToLocal(&result));
|
||||
ASSERT_TRUE(helper_
|
||||
.RunScript(context, script, "foo",
|
||||
base::span<v8::Local<v8::Value>>(), error_msg)
|
||||
.ToLocal(&result));
|
||||
EXPECT_FALSE(error_msg.has_value());
|
||||
int int_result = 0;
|
||||
ASSERT_TRUE(gin::ConvertFromV8(helper_.isolate(), result, &int_result));
|
||||
EXPECT_EQ(1, int_result);
|
||||
@@ -111,11 +147,116 @@ TEST_F(AuctionV8HelperTest, NoTime) {
|
||||
|
||||
// Make sure Date() is not accessible.
|
||||
v8::Local<v8::UnboundScript> script;
|
||||
base::Optional<std::string> error_msg;
|
||||
ASSERT_TRUE(helper_
|
||||
.Compile("function foo() { return Date();}",
|
||||
GURL("https://foo.test/"))
|
||||
GURL("https://foo.test/"), error_msg)
|
||||
.ToLocal(&script));
|
||||
EXPECT_TRUE(helper_.RunScript(context, script, "foo").IsEmpty());
|
||||
EXPECT_FALSE(error_msg.has_value());
|
||||
EXPECT_TRUE(helper_
|
||||
.RunScript(context, script, "foo",
|
||||
base::span<v8::Local<v8::Value>>(), error_msg)
|
||||
.IsEmpty());
|
||||
ASSERT_TRUE(error_msg.has_value());
|
||||
EXPECT_THAT(error_msg.value(), StartsWith("https://foo.test/:1"));
|
||||
EXPECT_THAT(error_msg.value(), HasSubstr("ReferenceError"));
|
||||
EXPECT_THAT(error_msg.value(), HasSubstr("Date"));
|
||||
}
|
||||
|
||||
// A script that doesn't compile.
|
||||
TEST_F(AuctionV8HelperTest, CompileError) {
|
||||
v8::Local<v8::UnboundScript> script;
|
||||
v8::Context::Scope ctx(helper_.scratch_context());
|
||||
base::Optional<std::string> error_msg;
|
||||
ASSERT_FALSE(
|
||||
helper_.Compile("function foo() { ", GURL("https://foo.test/"), error_msg)
|
||||
.ToLocal(&script));
|
||||
ASSERT_TRUE(error_msg.has_value());
|
||||
EXPECT_THAT(error_msg.value(), StartsWith("https://foo.test/:1 "));
|
||||
EXPECT_THAT(error_msg.value(), HasSubstr("SyntaxError"));
|
||||
}
|
||||
|
||||
// Test for exception at runtime at top-level.
|
||||
TEST_F(AuctionV8HelperTest, RunErrorTopLevel) {
|
||||
v8::Local<v8::UnboundScript> script;
|
||||
{
|
||||
v8::Context::Scope ctx(helper_.scratch_context());
|
||||
base::Optional<std::string> error_msg;
|
||||
ASSERT_TRUE(helper_
|
||||
.Compile("\n\nthrow new Error('I am an error');",
|
||||
GURL("https://foo.test/"), error_msg)
|
||||
.ToLocal(&script));
|
||||
EXPECT_FALSE(error_msg.has_value());
|
||||
}
|
||||
|
||||
v8::Local<v8::Context> context = helper_.CreateContext();
|
||||
base::Optional<std::string> error_msg;
|
||||
v8::Context::Scope ctx(context);
|
||||
v8::Local<v8::Value> result;
|
||||
ASSERT_FALSE(helper_
|
||||
.RunScript(context, script, "foo",
|
||||
base::span<v8::Local<v8::Value>>(), error_msg)
|
||||
.ToLocal(&result));
|
||||
ASSERT_TRUE(error_msg.has_value());
|
||||
EXPECT_EQ("https://foo.test/:3 Uncaught Error: I am an error.",
|
||||
error_msg.value());
|
||||
}
|
||||
|
||||
// Test for when desired function isn't found
|
||||
TEST_F(AuctionV8HelperTest, TargetFunctionNotFound) {
|
||||
v8::Local<v8::UnboundScript> script;
|
||||
{
|
||||
v8::Context::Scope ctx(helper_.scratch_context());
|
||||
base::Optional<std::string> error_msg;
|
||||
ASSERT_TRUE(helper_
|
||||
.Compile("function foo() { return 1;}",
|
||||
GURL("https://foo.test/"), error_msg)
|
||||
.ToLocal(&script));
|
||||
EXPECT_FALSE(error_msg.has_value());
|
||||
}
|
||||
|
||||
v8::Local<v8::Context> context = helper_.CreateContext();
|
||||
|
||||
base::Optional<std::string> error_msg;
|
||||
v8::Context::Scope ctx(context);
|
||||
v8::Local<v8::Value> result;
|
||||
ASSERT_FALSE(helper_
|
||||
.RunScript(context, script, "bar",
|
||||
base::span<v8::Local<v8::Value>>(), error_msg)
|
||||
.ToLocal(&result));
|
||||
ASSERT_TRUE(error_msg.has_value());
|
||||
|
||||
// This "not a function" and not "not found" since the lookup successfully
|
||||
// returns `undefined`.
|
||||
EXPECT_EQ("https://foo.test/ `bar` is not a function.", error_msg.value());
|
||||
}
|
||||
|
||||
TEST_F(AuctionV8HelperTest, TargetFunctionError) {
|
||||
v8::Local<v8::UnboundScript> script;
|
||||
{
|
||||
v8::Context::Scope ctx(helper_.scratch_context());
|
||||
base::Optional<std::string> error_msg;
|
||||
ASSERT_TRUE(helper_
|
||||
.Compile("function foo() { return notfound;}",
|
||||
GURL("https://foo.test/"), error_msg)
|
||||
.ToLocal(&script));
|
||||
EXPECT_FALSE(error_msg.has_value());
|
||||
}
|
||||
|
||||
v8::Local<v8::Context> context = helper_.CreateContext();
|
||||
|
||||
base::Optional<std::string> error_msg;
|
||||
v8::Context::Scope ctx(context);
|
||||
v8::Local<v8::Value> result;
|
||||
ASSERT_FALSE(helper_
|
||||
.RunScript(context, script, "foo",
|
||||
base::span<v8::Local<v8::Value>>(), error_msg)
|
||||
.ToLocal(&result));
|
||||
ASSERT_TRUE(error_msg.has_value());
|
||||
|
||||
EXPECT_THAT(error_msg.value(), StartsWith("https://foo.test/:1 "));
|
||||
EXPECT_THAT(error_msg.value(), HasSubstr("ReferenceError"));
|
||||
EXPECT_THAT(error_msg.value(), HasSubstr("notfound"));
|
||||
}
|
||||
|
||||
} // namespace auction_worklet
|
||||
|
@@ -14,6 +14,7 @@
|
||||
#include "base/callback.h"
|
||||
#include "base/logging.h"
|
||||
#include "base/stl_util.h"
|
||||
#include "base/strings/strcat.h"
|
||||
#include "base/time/time.h"
|
||||
#include "content/services/auction_worklet/auction_v8_helper.h"
|
||||
#include "content/services/auction_worklet/public/mojom/auction_worklet_service.mojom.h"
|
||||
@@ -59,6 +60,17 @@ BidderWorklet::BidResult::BidResult(std::string ad, double bid, GURL render_url)
|
||||
DCHECK(this->render_url.is_valid());
|
||||
}
|
||||
|
||||
BidderWorklet::BidResult::BidResult(base::Optional<std::string> error_msg)
|
||||
: error_msg(std::move(error_msg)) {}
|
||||
|
||||
BidderWorklet::BidResult::BidResult(const BidResult& other) = default;
|
||||
BidderWorklet::BidResult::BidResult(BidResult&& other) = default;
|
||||
BidderWorklet::BidResult::~BidResult() = default;
|
||||
BidderWorklet::BidResult& BidderWorklet::BidResult::operator=(
|
||||
const BidResult&) = default;
|
||||
BidderWorklet::BidResult& BidderWorklet::BidResult::operator=(BidResult&&) =
|
||||
default;
|
||||
|
||||
BidderWorklet::ReportWinResult::ReportWinResult() = default;
|
||||
|
||||
BidderWorklet::ReportWinResult::ReportWinResult(GURL report_url)
|
||||
@@ -66,6 +78,20 @@ BidderWorklet::ReportWinResult::ReportWinResult(GURL report_url)
|
||||
DCHECK(this->report_url.is_valid());
|
||||
}
|
||||
|
||||
BidderWorklet::ReportWinResult::ReportWinResult(
|
||||
base::Optional<std::string> error_msg)
|
||||
: error_msg(std::move(error_msg)) {}
|
||||
|
||||
BidderWorklet::ReportWinResult::ReportWinResult(const ReportWinResult& other) =
|
||||
default;
|
||||
BidderWorklet::ReportWinResult::ReportWinResult(ReportWinResult&& other) =
|
||||
default;
|
||||
BidderWorklet::ReportWinResult::~ReportWinResult() = default;
|
||||
BidderWorklet::ReportWinResult& BidderWorklet::ReportWinResult::operator=(
|
||||
const ReportWinResult&) = default;
|
||||
BidderWorklet::ReportWinResult& BidderWorklet::ReportWinResult::operator=(
|
||||
ReportWinResult&&) = default;
|
||||
|
||||
BidderWorklet::BidderWorklet(
|
||||
network::mojom::URLLoaderFactory* url_loader_factory,
|
||||
mojom::BiddingInterestGroupPtr bidding_interest_group,
|
||||
@@ -76,7 +102,9 @@ BidderWorklet::BidderWorklet(
|
||||
base::Time auction_start_time,
|
||||
AuctionV8Helper* v8_helper,
|
||||
LoadWorkletCallback load_worklet_callback)
|
||||
: v8_helper_(v8_helper),
|
||||
: script_source_url_(
|
||||
bidding_interest_group->group->bidding_url.value_or(GURL())),
|
||||
v8_helper_(v8_helper),
|
||||
bidding_interest_group_(std::move(bidding_interest_group)),
|
||||
auction_signals_json_(auction_signals_json),
|
||||
per_buyer_signals_json_(per_buyer_signals_json),
|
||||
@@ -85,11 +113,10 @@ BidderWorklet::BidderWorklet(
|
||||
browser_signal_seller_(browser_signal_seller),
|
||||
auction_start_time_(auction_start_time) {
|
||||
DCHECK(load_worklet_callback);
|
||||
// TODO(mmenke): Remove up the value_or() - auction worklets shouldn't be
|
||||
// created when there's no bidding URL.
|
||||
// TODO(mmenke): Remove up the value_or() for script_source_url_- auction
|
||||
// worklets shouldn't be created when there's no bidding URL.
|
||||
worklet_loader_ = std::make_unique<WorkletLoader>(
|
||||
url_loader_factory,
|
||||
bidding_interest_group_->group->bidding_url.value_or(GURL()), v8_helper,
|
||||
url_loader_factory, script_source_url_, v8_helper,
|
||||
base::BindOnce(&BidderWorklet::OnDownloadComplete, base::Unretained(this),
|
||||
std::move(load_worklet_callback)));
|
||||
}
|
||||
@@ -202,12 +229,18 @@ BidderWorklet::BidResult BidderWorklet::GenerateBid(
|
||||
args.push_back(browser_signals);
|
||||
|
||||
v8::Local<v8::Value> generate_bid_result;
|
||||
base::Optional<std::string> error_msg_out;
|
||||
if (!v8_helper_
|
||||
->RunScript(context, worklet_script_->Get(isolate), "generateBid",
|
||||
args)
|
||||
.ToLocal(&generate_bid_result) ||
|
||||
!generate_bid_result->IsObject()) {
|
||||
return BidResult();
|
||||
args, error_msg_out)
|
||||
.ToLocal(&generate_bid_result)) {
|
||||
return BidResult(std::move(error_msg_out));
|
||||
}
|
||||
|
||||
if (!generate_bid_result->IsObject()) {
|
||||
return BidResult(
|
||||
base::StrCat({script_source_url_.spec(),
|
||||
" generateBid() return value not an object."}));
|
||||
}
|
||||
|
||||
gin::Dictionary result_dict(isolate, generate_bid_result.As<v8::Object>());
|
||||
@@ -221,22 +254,29 @@ BidderWorklet::BidResult BidderWorklet::GenerateBid(
|
||||
!v8_helper_->ExtractJson(context, ad_object, &ad_json) ||
|
||||
!result_dict.Get("bid", &bid) ||
|
||||
!result_dict.Get("render", &render_url_string)) {
|
||||
return BidResult();
|
||||
return BidResult(
|
||||
base::StrCat({script_source_url_.spec(),
|
||||
" generateBid() return value has incorrect structure."}));
|
||||
}
|
||||
|
||||
if (bid <= 0 || std::isnan(bid) || !std::isfinite(bid))
|
||||
return BidResult();
|
||||
|
||||
GURL render_url(render_url_string);
|
||||
if (!render_url.is_valid() || !render_url.SchemeIs(url::kHttpsScheme))
|
||||
return BidResult();
|
||||
if (!render_url.is_valid() || !render_url.SchemeIs(url::kHttpsScheme)) {
|
||||
return BidResult(base::StrCat(
|
||||
{script_source_url_.spec(),
|
||||
" generateBid() returned render_url isn't a valid https:// URL."}));
|
||||
}
|
||||
|
||||
// `render_url` must be in `ad_render_urls`.
|
||||
for (const auto& ad : *interest_group.ads) {
|
||||
if (render_url == ad->render_url)
|
||||
return BidResult(std::move(ad_json), bid, std::move(render_url));
|
||||
}
|
||||
return BidResult();
|
||||
return BidResult(base::StrCat({script_source_url_.spec(),
|
||||
" generateBid() returned render_url isn't one "
|
||||
"of the registered creative URLs."}));
|
||||
}
|
||||
|
||||
BidderWorklet::ReportWinResult BidderWorklet::ReportWin(
|
||||
@@ -285,10 +325,12 @@ BidderWorklet::ReportWinResult BidderWorklet::ReportWin(
|
||||
|
||||
// An empty return value indicates an exception was thrown. Any other return
|
||||
// value indicates no exception.
|
||||
base::Optional<std::string> error_msg_out;
|
||||
if (v8_helper_
|
||||
->RunScript(context, worklet_script_->Get(isolate), "reportWin", args)
|
||||
->RunScript(context, worklet_script_->Get(isolate), "reportWin", args,
|
||||
error_msg_out)
|
||||
.IsEmpty()) {
|
||||
return ReportWinResult();
|
||||
return ReportWinResult(std::move(error_msg_out));
|
||||
}
|
||||
|
||||
if (!report_bindings.report_url().is_valid())
|
||||
@@ -299,10 +341,12 @@ BidderWorklet::ReportWinResult BidderWorklet::ReportWin(
|
||||
|
||||
void BidderWorklet::OnDownloadComplete(
|
||||
LoadWorkletCallback load_worklet_callback,
|
||||
std::unique_ptr<v8::Global<v8::UnboundScript>> worklet_script) {
|
||||
std::unique_ptr<v8::Global<v8::UnboundScript>> worklet_script,
|
||||
base::Optional<std::string> error_msg) {
|
||||
worklet_loader_.reset();
|
||||
worklet_script_ = std::move(worklet_script);
|
||||
std::move(load_worklet_callback).Run(worklet_script_ != nullptr);
|
||||
std::move(load_worklet_callback)
|
||||
.Run(worklet_script_ != nullptr, std::move(error_msg));
|
||||
}
|
||||
|
||||
} // namespace auction_worklet
|
||||
|
@@ -49,12 +49,21 @@ class BidderWorklet {
|
||||
// valid.
|
||||
BidResult(std::string ad, double bid, GURL render_url);
|
||||
|
||||
// Constructor when there is no bid due to an error and an error message
|
||||
// may be available.
|
||||
explicit BidResult(base::Optional<std::string> error_msg);
|
||||
|
||||
BidResult(const BidResult& other);
|
||||
BidResult(BidResult&& other);
|
||||
|
||||
~BidResult();
|
||||
|
||||
BidResult& operator=(const BidResult&);
|
||||
BidResult& operator=(BidResult&&);
|
||||
|
||||
// `success` will be false on any type of failure, and other values will be
|
||||
// empty or 0. Inability to extract ad string, bid value, or valid render
|
||||
// URL are all considered errors.
|
||||
//
|
||||
// TODO(mmenke): Pass along some sort of error string instead, to make
|
||||
// debugging easier.
|
||||
bool success = false;
|
||||
|
||||
// JSON string to be passed to the scoring function.
|
||||
@@ -66,6 +75,10 @@ class BidderWorklet {
|
||||
|
||||
// Render URL, if any bid was made.
|
||||
GURL render_url;
|
||||
|
||||
// Error message for debugging. This isn't guaranteed to be produced for all
|
||||
// failures, so don't check this instead of `success`.
|
||||
base::Optional<std::string> error_msg;
|
||||
};
|
||||
|
||||
struct ReportWinResult {
|
||||
@@ -76,19 +89,33 @@ class BidderWorklet {
|
||||
// Constructor when a report was requested.
|
||||
explicit ReportWinResult(GURL report_url);
|
||||
|
||||
// Constructor when there is some sort of a falire for which an error
|
||||
// message may be available.
|
||||
explicit ReportWinResult(base::Optional<std::string> error_msg);
|
||||
|
||||
ReportWinResult(const ReportWinResult& other);
|
||||
ReportWinResult(ReportWinResult&& other);
|
||||
|
||||
~ReportWinResult();
|
||||
|
||||
ReportWinResult& operator=(const ReportWinResult&);
|
||||
ReportWinResult& operator=(ReportWinResult&&);
|
||||
|
||||
// `success` will be false on any type of failure. Neither lack or reporting
|
||||
// function nor lack of report URL is considered an error.
|
||||
//
|
||||
// TODO(mmenke): Pass along some sort of error string instead, to make
|
||||
// debugging easier.
|
||||
bool success = false;
|
||||
|
||||
// Report URL, if one is provided. Empty on failure, or if no report URL is
|
||||
// provided.
|
||||
GURL report_url;
|
||||
|
||||
// Error message for debugging.
|
||||
base::Optional<std::string> error_msg;
|
||||
};
|
||||
|
||||
using LoadWorkletCallback = base::OnceCallback<void(bool success)>;
|
||||
using LoadWorkletCallback =
|
||||
base::OnceCallback<void(bool success,
|
||||
base::Optional<std::string> error_msg)>;
|
||||
|
||||
// Starts loading the worklet script on construction. Callback will be invoked
|
||||
// asynchronously once the data has been fetched or an error has occurred.
|
||||
@@ -123,8 +150,10 @@ class BidderWorklet {
|
||||
private:
|
||||
void OnDownloadComplete(
|
||||
LoadWorkletCallback load_worklet_callback,
|
||||
std::unique_ptr<v8::Global<v8::UnboundScript>> worklet_script);
|
||||
std::unique_ptr<v8::Global<v8::UnboundScript>> worklet_script,
|
||||
base::Optional<std::string> error_msg);
|
||||
|
||||
const GURL script_source_url_;
|
||||
AuctionV8Helper* const v8_helper_;
|
||||
const mojom::BiddingInterestGroupPtr bidding_interest_group_;
|
||||
|
||||
|
@@ -20,11 +20,15 @@
|
||||
#include "mojo/public/cpp/bindings/struct_ptr.h"
|
||||
#include "net/http/http_status_code.h"
|
||||
#include "services/network/test/test_url_loader_factory.h"
|
||||
#include "testing/gmock/include/gmock/gmock-matchers.h"
|
||||
#include "testing/gtest/include/gtest/gtest.h"
|
||||
#include "third_party/blink/public/mojom/interest_group/interest_group_types.mojom.h"
|
||||
#include "url/gurl.h"
|
||||
#include "url/origin.h"
|
||||
|
||||
using testing::HasSubstr;
|
||||
using testing::StartsWith;
|
||||
|
||||
namespace auction_worklet {
|
||||
namespace {
|
||||
|
||||
@@ -135,31 +139,41 @@ class BidderWorkletTest : public testing::Test {
|
||||
EXPECT_EQ(expected_result.ad, actual_result.ad);
|
||||
EXPECT_EQ(expected_result.bid, actual_result.bid);
|
||||
EXPECT_EQ(expected_result.render_url, actual_result.render_url);
|
||||
EXPECT_EQ(expected_result.error_msg.has_value(),
|
||||
actual_result.error_msg.has_value());
|
||||
EXPECT_EQ(expected_result.error_msg.value_or("Not an error"),
|
||||
actual_result.error_msg.value_or("Not an error"));
|
||||
}
|
||||
|
||||
// Configures `url_loader_factory_` to return a reportWin() script with the
|
||||
// specified body. Then runs the script, expecting the provided result.
|
||||
void RunReportWinWithFunctionBodyExpectingResult(
|
||||
const std::string& function_body,
|
||||
const GURL& expected_report_url) {
|
||||
const GURL& expected_report_url,
|
||||
base::Optional<std::string> expected_error_msg = base::nullopt) {
|
||||
RunReportWinWithJavascriptExpectingResult(
|
||||
CreateReportWinScript(function_body), expected_report_url);
|
||||
CreateReportWinScript(function_body), expected_report_url,
|
||||
std::move(expected_error_msg));
|
||||
}
|
||||
|
||||
// Configures `url_loader_factory_` to return a reportWin() script with the
|
||||
// specified Javascript. Then runs the script, expecting the provided result.
|
||||
void RunReportWinWithJavascriptExpectingResult(
|
||||
const std::string& javascript,
|
||||
const GURL& expected_report_url) {
|
||||
const GURL& expected_report_url,
|
||||
base::Optional<std::string> expected_error_msg = base::nullopt) {
|
||||
SCOPED_TRACE(javascript);
|
||||
AddJavascriptResponse(&url_loader_factory_, interest_group_bidding_url_,
|
||||
javascript);
|
||||
RunReportWinExpectingResult(expected_report_url);
|
||||
RunReportWinExpectingResult(expected_report_url,
|
||||
std::move(expected_error_msg));
|
||||
}
|
||||
|
||||
// Loads and runs a reportWin() with the provided return line, expecting the
|
||||
// supplied result.
|
||||
void RunReportWinExpectingResult(const GURL& expected_report_url) {
|
||||
void RunReportWinExpectingResult(
|
||||
const GURL& expected_report_url,
|
||||
base::Optional<std::string> expected_error_msg = base::nullopt) {
|
||||
auto bidder_worket = CreateWorklet();
|
||||
ASSERT_TRUE(bidder_worket);
|
||||
|
||||
@@ -168,6 +182,10 @@ class BidderWorkletTest : public testing::Test {
|
||||
browser_signal_ad_render_fingerprint_, browser_signal_bid_);
|
||||
EXPECT_EQ(!expected_report_url.is_empty(), actual_result.success);
|
||||
EXPECT_EQ(expected_report_url, actual_result.report_url);
|
||||
EXPECT_EQ(expected_error_msg.has_value(),
|
||||
actual_result.error_msg.has_value());
|
||||
EXPECT_EQ(expected_error_msg.value_or("Not an error"),
|
||||
actual_result.error_msg.value_or("Not an error"));
|
||||
}
|
||||
|
||||
// Create a BidderWorklet, waiting for the URLLoader to complete. Returns
|
||||
@@ -231,11 +249,19 @@ class BidderWorkletTest : public testing::Test {
|
||||
return out;
|
||||
}
|
||||
|
||||
void CreateWorkletCallback(bool success) {
|
||||
void CreateWorkletCallback(bool success,
|
||||
base::Optional<std::string> error_msg) {
|
||||
create_worklet_succeeded_ = success;
|
||||
error_msg_ = std::move(error_msg);
|
||||
if (success)
|
||||
EXPECT_FALSE(error_msg_.has_value());
|
||||
load_script_run_loop_->Quit();
|
||||
}
|
||||
|
||||
std::string last_error_msg() const {
|
||||
return error_msg_.value_or("Not an error");
|
||||
}
|
||||
|
||||
base::test::TaskEnvironment task_environment_;
|
||||
|
||||
// Values used to construct the BiddingInterestGroup passed to the
|
||||
@@ -277,6 +303,7 @@ class BidderWorkletTest : public testing::Test {
|
||||
// synchronously.
|
||||
std::unique_ptr<base::RunLoop> load_script_run_loop_;
|
||||
bool create_worklet_succeeded_ = false;
|
||||
base::Optional<std::string> error_msg_;
|
||||
|
||||
network::TestURLLoaderFactory url_loader_factory_;
|
||||
AuctionV8Helper v8_helper_;
|
||||
@@ -287,12 +314,17 @@ TEST_F(BidderWorkletTest, NetworkError) {
|
||||
CreateBasicGenerateBidScript(),
|
||||
net::HTTP_NOT_FOUND);
|
||||
EXPECT_FALSE(CreateWorklet());
|
||||
EXPECT_EQ("Failed to load https://url.test/ HTTP status = 404 Not Found.",
|
||||
last_error_msg());
|
||||
}
|
||||
|
||||
TEST_F(BidderWorkletTest, CompileError) {
|
||||
AddJavascriptResponse(&url_loader_factory_, interest_group_bidding_url_,
|
||||
"Invalid Javascript");
|
||||
EXPECT_FALSE(CreateWorklet());
|
||||
|
||||
EXPECT_THAT(last_error_msg(), StartsWith("https://url.test/:1 "));
|
||||
EXPECT_THAT(last_error_msg(), HasSubstr("SyntaxError"));
|
||||
}
|
||||
|
||||
// Test parsing of return values.
|
||||
@@ -335,10 +367,12 @@ TEST_F(BidderWorkletTest, GenerateBidResult) {
|
||||
// Other values JSON can't represent result in failing instead of null.
|
||||
RunGenerateBidWithReturnValueExpectingResult(
|
||||
R"({ad: globalThis.not_defined, bid:1, render:"https://response.test/"})",
|
||||
BidderWorklet::BidResult());
|
||||
BidderWorklet::BidResult("https://url.test/ generateBid() return value "
|
||||
"has incorrect structure."));
|
||||
RunGenerateBidWithReturnValueExpectingResult(
|
||||
R"({ad: function() {return 1;}, bid:1, render:"https://response.test/"})",
|
||||
BidderWorklet::BidResult());
|
||||
BidderWorklet::BidResult("https://url.test/ generateBid() return value "
|
||||
"has incorrect structure."));
|
||||
|
||||
// Make sure recursive structures aren't allowed in ad field.
|
||||
RunGenerateBidWithJavascriptExpectingResult(
|
||||
@@ -349,7 +383,8 @@ TEST_F(BidderWorkletTest, GenerateBidResult) {
|
||||
return {ad: a, bid:1, render:"https://response.test/"};
|
||||
}
|
||||
)",
|
||||
BidderWorklet::BidResult());
|
||||
BidderWorklet::BidResult("https://url.test/ generateBid() return value "
|
||||
"has incorrect structure."));
|
||||
|
||||
// --------
|
||||
// Vary bid
|
||||
@@ -392,10 +427,12 @@ TEST_F(BidderWorkletTest, GenerateBidResult) {
|
||||
// Non-numeric bid.
|
||||
RunGenerateBidWithReturnValueExpectingResult(
|
||||
R"({ad: ["ad"], bid:"1", render:"https://response.test/"})",
|
||||
BidderWorklet::BidResult());
|
||||
BidderWorklet::BidResult("https://url.test/ generateBid() return value "
|
||||
"has incorrect structure."));
|
||||
RunGenerateBidWithReturnValueExpectingResult(
|
||||
R"({ad: ["ad"], bid:[1], render:"https://response.test/"})",
|
||||
BidderWorklet::BidResult());
|
||||
BidderWorklet::BidResult("https://url.test/ generateBid() return value "
|
||||
"has incorrect structure."));
|
||||
|
||||
// ---------
|
||||
// Vary URL.
|
||||
@@ -408,43 +445,61 @@ TEST_F(BidderWorkletTest, GenerateBidResult) {
|
||||
// Disallowed schemes.
|
||||
RunGenerateBidWithReturnValueExpectingResult(
|
||||
R"({ad: ["ad"], bid:1, render:"http://response.test/"})",
|
||||
BidderWorklet::BidResult());
|
||||
BidderWorklet::BidResult("https://url.test/ generateBid() returned "
|
||||
"render_url isn't a valid https:// URL."));
|
||||
RunGenerateBidWithReturnValueExpectingResult(
|
||||
R"({ad: ["ad"], bid:1, render:"chrome-extension://response.test/"})",
|
||||
BidderWorklet::BidResult());
|
||||
BidderWorklet::BidResult("https://url.test/ generateBid() returned "
|
||||
"render_url isn't a valid https:// URL."));
|
||||
RunGenerateBidWithReturnValueExpectingResult(
|
||||
R"({ad: ["ad"], bid:1, render:"about:blank"})",
|
||||
BidderWorklet::BidResult());
|
||||
BidderWorklet::BidResult("https://url.test/ generateBid() returned "
|
||||
"render_url isn't a valid https:// URL."));
|
||||
RunGenerateBidWithReturnValueExpectingResult(
|
||||
R"({ad: ["ad"], bid:1, render:"data:,foo"})", BidderWorklet::BidResult());
|
||||
R"({ad: ["ad"], bid:1, render:"data:,foo"})",
|
||||
BidderWorklet::BidResult("https://url.test/ generateBid() returned "
|
||||
"render_url isn't a valid https:// URL."));
|
||||
|
||||
// Invalid URLs.
|
||||
RunGenerateBidWithReturnValueExpectingResult(
|
||||
R"({ad: ["ad"], bid:1, render:"test"})", BidderWorklet::BidResult());
|
||||
R"({ad: ["ad"], bid:1, render:"test"})",
|
||||
BidderWorklet::BidResult("https://url.test/ generateBid() returned "
|
||||
"render_url isn't a valid https:// URL."));
|
||||
RunGenerateBidWithReturnValueExpectingResult(
|
||||
R"({ad: ["ad"], bid:1, render:"http://"})", BidderWorklet::BidResult());
|
||||
R"({ad: ["ad"], bid:1, render:"http://"})",
|
||||
BidderWorklet::BidResult("https://url.test/ generateBid() returned "
|
||||
"render_url isn't a valid https:// URL."));
|
||||
RunGenerateBidWithReturnValueExpectingResult(
|
||||
R"({ad: ["ad"], bid:1, render:["http://response.test/"]})",
|
||||
BidderWorklet::BidResult());
|
||||
BidderWorklet::BidResult("https://url.test/ generateBid() return value "
|
||||
"has incorrect structure."));
|
||||
RunGenerateBidWithReturnValueExpectingResult(
|
||||
R"({ad: ["ad"], bid:1, render:9})", BidderWorklet::BidResult());
|
||||
R"({ad: ["ad"], bid:1, render:9})",
|
||||
BidderWorklet::BidResult("https://url.test/ generateBid() return value "
|
||||
"has incorrect structure."));
|
||||
|
||||
// ------------
|
||||
// Other cases.
|
||||
// ------------
|
||||
|
||||
// No return value.
|
||||
RunGenerateBidWithReturnValueExpectingResult("", BidderWorklet::BidResult());
|
||||
RunGenerateBidWithReturnValueExpectingResult(
|
||||
"", BidderWorklet::BidResult(
|
||||
"https://url.test/ generateBid() return value not an object."));
|
||||
|
||||
// Missing value.
|
||||
RunGenerateBidWithReturnValueExpectingResult(
|
||||
R"({bid:"a", render:"https://response.test/"})",
|
||||
BidderWorklet::BidResult());
|
||||
BidderWorklet::BidResult("https://url.test/ generateBid() return value "
|
||||
"has incorrect structure."));
|
||||
RunGenerateBidWithReturnValueExpectingResult(
|
||||
R"({ad: ["ad"], render:"https://response.test/"})",
|
||||
BidderWorklet::BidResult());
|
||||
RunGenerateBidWithReturnValueExpectingResult(R"({ad: ["ad"], bid:"a"})",
|
||||
BidderWorklet::BidResult());
|
||||
BidderWorklet::BidResult("https://url.test/ generateBid() return value "
|
||||
"has incorrect structure."));
|
||||
RunGenerateBidWithReturnValueExpectingResult(
|
||||
R"({ad: ["ad"], bid:"a"})",
|
||||
BidderWorklet::BidResult("https://url.test/ generateBid() return value "
|
||||
"has incorrect structure."));
|
||||
|
||||
// Valid JS, but missing function.
|
||||
RunGenerateBidWithJavascriptExpectingResult(
|
||||
@@ -453,22 +508,28 @@ TEST_F(BidderWorkletTest, GenerateBidResult) {
|
||||
return {ad: ["ad"], bid:1, render:"https://response.test/"};
|
||||
}
|
||||
)",
|
||||
BidderWorklet::BidResult());
|
||||
RunGenerateBidWithJavascriptExpectingResult("", BidderWorklet::BidResult());
|
||||
RunGenerateBidWithJavascriptExpectingResult("5", BidderWorklet::BidResult());
|
||||
RunGenerateBidWithJavascriptExpectingResult("shrimp",
|
||||
BidderWorklet::BidResult());
|
||||
BidderWorklet::BidResult(
|
||||
"https://url.test/ `generateBid` is not a function."));
|
||||
RunGenerateBidWithJavascriptExpectingResult(
|
||||
"", BidderWorklet::BidResult(
|
||||
"https://url.test/ `generateBid` is not a function."));
|
||||
RunGenerateBidWithJavascriptExpectingResult(
|
||||
"5", BidderWorklet::BidResult(
|
||||
"https://url.test/ `generateBid` is not a function."));
|
||||
|
||||
// Throw exception.
|
||||
RunGenerateBidWithReturnValueExpectingResult("shrimp",
|
||||
BidderWorklet::BidResult());
|
||||
RunGenerateBidWithJavascriptExpectingResult(
|
||||
"shrimp",
|
||||
BidderWorklet::BidResult("https://url.test/:1 Uncaught ReferenceError: "
|
||||
"shrimp is not defined."));
|
||||
}
|
||||
|
||||
// Make sure Date() is not available when running generateBid().
|
||||
TEST_F(BidderWorkletTest, GenerateBidDateNotAvailable) {
|
||||
RunGenerateBidWithReturnValueExpectingResult(
|
||||
R"({ad: Date().toString(), bid:1, render:"https://response.test/"})",
|
||||
BidderWorklet::BidResult());
|
||||
BidderWorklet::BidResult(
|
||||
"https://url.test/:4 Uncaught ReferenceError: Date is not defined."));
|
||||
}
|
||||
|
||||
// Checks that most input parameters are correctly passed in, and each is parsed
|
||||
@@ -610,7 +671,9 @@ TEST_F(BidderWorkletTest, GenerateBidBasicInputParameters) {
|
||||
// A bid URL that's not in the InterestGroup's ads list should fail.
|
||||
RunGenerateBidWithReturnValueExpectingResult(
|
||||
R"({ad: 0, bid:1, render:"https://response2.test/"})",
|
||||
BidderWorklet::BidResult());
|
||||
BidderWorklet::BidResult(
|
||||
"https://url.test/ generateBid() returned render_url isn't one of "
|
||||
"the registered creative URLs."));
|
||||
|
||||
// Adding an ad with a corresponding `renderUrl` should result in success.
|
||||
// Also check the `interestGroup.ads` field passed to Javascript.
|
||||
@@ -784,10 +847,12 @@ TEST_F(BidderWorkletTest, GenerateBidTrustedBiddingSignals) {
|
||||
trusted_bidding_signals_ = std::make_unique<TrustedBiddingSignals>(
|
||||
&url_loader_factory_, std::vector<std::string>({"key1", "key2"}),
|
||||
"hostname", kBaseSignalsUrl, &v8_helper_,
|
||||
base::BindLambdaForTesting([&](bool success) {
|
||||
signals_loaded_successfully = success;
|
||||
run_loop.Quit();
|
||||
}));
|
||||
base::BindLambdaForTesting(
|
||||
[&](bool success, base::Optional<std::string> signals_error_msg) {
|
||||
signals_loaded_successfully = success;
|
||||
EXPECT_FALSE(signals_error_msg.has_value());
|
||||
run_loop.Quit();
|
||||
}));
|
||||
run_loop.Run();
|
||||
ASSERT_TRUE(signals_loaded_successfully);
|
||||
|
||||
@@ -827,21 +892,31 @@ TEST_F(BidderWorkletTest, ReportWin) {
|
||||
R"(sendReportTo("https://foo.test/bar"))", GURL("https://foo.test/bar"));
|
||||
|
||||
RunReportWinWithFunctionBodyExpectingResult(
|
||||
R"(sendReportTo("http://http.not.allowed.test"))", GURL());
|
||||
R"(sendReportTo("http://http.not.allowed.test"))", GURL(),
|
||||
"https://url.test/:4 Uncaught TypeError: sendReportTo must be passed a "
|
||||
"valid HTTPS url.");
|
||||
RunReportWinWithFunctionBodyExpectingResult(
|
||||
R"(sendReportTo("file:///file.not.allowed.test"))", GURL());
|
||||
R"(sendReportTo("file:///file.not.allowed.test"))", GURL(),
|
||||
"https://url.test/:4 Uncaught TypeError: sendReportTo must be passed a "
|
||||
"valid HTTPS url.");
|
||||
|
||||
RunReportWinWithFunctionBodyExpectingResult(R"(sendReportTo(""))", GURL());
|
||||
RunReportWinWithFunctionBodyExpectingResult(
|
||||
R"(sendReportTo(""))", GURL(),
|
||||
"https://url.test/:4 Uncaught TypeError: sendReportTo must be passed a "
|
||||
"valid HTTPS url.");
|
||||
|
||||
RunReportWinWithFunctionBodyExpectingResult(
|
||||
R"(sendReportTo("https://foo.test");sendReportTo("https://foo.test"))",
|
||||
GURL());
|
||||
GURL(),
|
||||
"https://url.test/:4 Uncaught TypeError: sendReportTo may be called at "
|
||||
"most once.");
|
||||
}
|
||||
|
||||
// Make sure Date() is not available when running reportWin().
|
||||
TEST_F(BidderWorkletTest, ReportWinDateNotAvailable) {
|
||||
RunReportWinWithFunctionBodyExpectingResult(
|
||||
R"(sendReportTo("https://foo.test/" + Date().toString()))", GURL());
|
||||
R"(sendReportTo("https://foo.test/" + Date().toString()))", GURL(),
|
||||
"https://url.test/:4 Uncaught ReferenceError: Date is not defined.");
|
||||
}
|
||||
|
||||
TEST_F(BidderWorkletTest, ReportWinParameters) {
|
||||
@@ -852,31 +927,49 @@ TEST_F(BidderWorkletTest, ReportWinParameters) {
|
||||
bool is_json;
|
||||
// Pointer to location at which the string can be modified.
|
||||
std::string* value_ptr;
|
||||
// Whether to expect an error. This can be empty when call fails in case
|
||||
// it's due to something like passing non-JSON to JSON parameter which user
|
||||
// code should be unable to trigger, and for which we thus do not produce
|
||||
// an error message.
|
||||
base::Optional<std::string> expect_error_msg;
|
||||
base::Optional<std::string> expect_error_msg_array;
|
||||
} kStringTestCases[] = {
|
||||
{
|
||||
"auctionSignals",
|
||||
true /* is_json */,
|
||||
&auction_signals_,
|
||||
base::nullopt,
|
||||
base::nullopt,
|
||||
},
|
||||
{
|
||||
"perBuyerSignals",
|
||||
true /* is_json */,
|
||||
&per_buyer_signals_,
|
||||
base::nullopt,
|
||||
base::nullopt,
|
||||
},
|
||||
{
|
||||
"sellerSignals",
|
||||
true /* is_json */,
|
||||
&seller_signals_,
|
||||
base::nullopt,
|
||||
base::nullopt,
|
||||
},
|
||||
{
|
||||
"browserSignals.interestGroupName",
|
||||
false /* is_json */,
|
||||
&interest_group_name_,
|
||||
base::nullopt,
|
||||
"https://url.test/:4 Uncaught TypeError: sendReportTo must be passed "
|
||||
"a valid HTTPS url.",
|
||||
},
|
||||
{
|
||||
"browserSignals.adRenderFingerprint",
|
||||
false /* is_json */,
|
||||
&browser_signal_ad_render_fingerprint_,
|
||||
base::nullopt,
|
||||
"https://url.test/:4 Uncaught TypeError: sendReportTo must be passed "
|
||||
"a valid HTTPS url.",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -886,12 +979,14 @@ TEST_F(BidderWorkletTest, ReportWinParameters) {
|
||||
*test_case.value_ptr = "https://foo.test/";
|
||||
RunReportWinWithFunctionBodyExpectingResult(
|
||||
base::StringPrintf("sendReportTo(%s)", test_case.name),
|
||||
test_case.is_json ? GURL() : GURL("https://foo.test/"));
|
||||
test_case.is_json ? GURL() : GURL("https://foo.test/"),
|
||||
test_case.expect_error_msg);
|
||||
|
||||
*test_case.value_ptr = R"(["https://foo.test/"])";
|
||||
RunReportWinWithFunctionBodyExpectingResult(
|
||||
base::StringPrintf("sendReportTo(%s[0])", test_case.name),
|
||||
test_case.is_json ? GURL("https://foo.test/") : GURL());
|
||||
test_case.is_json ? GURL("https://foo.test/") : GURL(),
|
||||
test_case.expect_error_msg_array);
|
||||
|
||||
SetDefaultParameters();
|
||||
}
|
||||
|
@@ -106,6 +106,9 @@ interface AuctionWorkletService {
|
||||
// `seller_report` indicates if the seller wishes to make a report,
|
||||
// and the URL that the seller wanted fetched for that.
|
||||
//
|
||||
// `errors` are various error messages to be used for debugging. These are too
|
||||
// sensitive for the renderers to see.
|
||||
//
|
||||
// TODO(mmenke): May be good to make some way to share worklets between
|
||||
// multiple auctions on the same page, but not auctions across pages.
|
||||
// Could create separate top level AuctionWorkletService objects (using the
|
||||
@@ -119,5 +122,6 @@ interface AuctionWorkletService {
|
||||
url.mojom.Origin winning_interest_group_owner,
|
||||
string winning_interest_group_name,
|
||||
WinningBidderReport bidder_report,
|
||||
SellerReport seller_report);
|
||||
SellerReport seller_report,
|
||||
array<string> errors);
|
||||
};
|
||||
|
@@ -45,7 +45,7 @@ void ReportBindings::SendReportTo(
|
||||
bindings->report_url_ = GURL();
|
||||
args.GetIsolate()->ThrowException(
|
||||
v8::Exception::TypeError(v8_helper->CreateStringFromLiteral(
|
||||
"SendReportTo requires 1 string parameter.")));
|
||||
"sendReportTo requires 1 string parameter")));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -54,7 +54,8 @@ void ReportBindings::SendReportTo(
|
||||
bindings->report_url_ = GURL();
|
||||
args.GetIsolate()->ThrowException(
|
||||
v8::Exception::TypeError(v8_helper->CreateStringFromLiteral(
|
||||
"SendReportTo may be called at most once.")));
|
||||
"sendReportTo may be called at most once")));
|
||||
return;
|
||||
}
|
||||
|
||||
GURL url(url_string);
|
||||
@@ -62,7 +63,7 @@ void ReportBindings::SendReportTo(
|
||||
bindings->exception_thrown_ = true;
|
||||
args.GetIsolate()->ThrowException(
|
||||
v8::Exception::TypeError(v8_helper->CreateStringFromLiteral(
|
||||
"SendReportTo must be passed a valid HTTPS url.")));
|
||||
"sendReportTo must be passed a valid HTTPS url")));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@@ -13,6 +13,7 @@
|
||||
#include "base/callback.h"
|
||||
#include "base/logging.h"
|
||||
#include "base/stl_util.h"
|
||||
#include "base/strings/strcat.h"
|
||||
#include "base/time/time.h"
|
||||
#include "content/services/auction_worklet/auction_v8_helper.h"
|
||||
#include "content/services/auction_worklet/public/mojom/auction_worklet_service.mojom.h"
|
||||
@@ -114,6 +115,17 @@ SellerWorklet::ScoreResult::ScoreResult(double score)
|
||||
DCHECK_GT(score, 0);
|
||||
}
|
||||
|
||||
SellerWorklet::ScoreResult::ScoreResult(base::Optional<std::string> error_msg)
|
||||
: error_msg(std::move(error_msg)) {}
|
||||
|
||||
SellerWorklet::ScoreResult::ScoreResult(const ScoreResult& other) = default;
|
||||
SellerWorklet::ScoreResult::ScoreResult(ScoreResult&& other) = default;
|
||||
SellerWorklet::ScoreResult::~ScoreResult() = default;
|
||||
SellerWorklet::ScoreResult& SellerWorklet::ScoreResult::operator=(
|
||||
const ScoreResult&) = default;
|
||||
SellerWorklet::ScoreResult& SellerWorklet::ScoreResult::operator=(
|
||||
ScoreResult&&) = default;
|
||||
|
||||
SellerWorklet::Report::Report() = default;
|
||||
|
||||
SellerWorklet::Report::Report(std::string signals_for_winner, GURL report_url)
|
||||
@@ -121,12 +133,22 @@ SellerWorklet::Report::Report(std::string signals_for_winner, GURL report_url)
|
||||
signals_for_winner(std::move(signals_for_winner)),
|
||||
report_url(std::move(report_url)) {}
|
||||
|
||||
SellerWorklet::Report::Report(base::Optional<std::string> error_msg)
|
||||
: error_msg(std::move(error_msg)) {}
|
||||
|
||||
SellerWorklet::Report::Report(const Report& other) = default;
|
||||
SellerWorklet::Report::Report(Report&& other) = default;
|
||||
SellerWorklet::Report::~Report() = default;
|
||||
SellerWorklet::Report& SellerWorklet::Report::operator=(const Report&) =
|
||||
default;
|
||||
SellerWorklet::Report& SellerWorklet::Report::operator=(Report&&) = default;
|
||||
|
||||
SellerWorklet::SellerWorklet(
|
||||
network::mojom::URLLoaderFactory* url_loader_factory,
|
||||
const GURL& script_source_url,
|
||||
AuctionV8Helper* v8_helper,
|
||||
LoadWorkletCallback load_worklet_callback)
|
||||
: v8_helper_(v8_helper) {
|
||||
: script_source_url_(script_source_url), v8_helper_(v8_helper) {
|
||||
DCHECK(load_worklet_callback);
|
||||
worklet_loader_ = std::make_unique<WorkletLoader>(
|
||||
url_loader_factory, script_source_url, v8_helper,
|
||||
@@ -181,14 +203,22 @@ SellerWorklet::ScoreResult SellerWorklet::ScoreAd(
|
||||
|
||||
v8::Local<v8::Value> score_ad_result;
|
||||
double score;
|
||||
base::Optional<std::string> error_msg_out;
|
||||
if (!v8_helper_
|
||||
->RunScript(context, worklet_script_->Get(isolate), "scoreAd", args)
|
||||
.ToLocal(&score_ad_result) ||
|
||||
!gin::ConvertFromV8(isolate, score_ad_result, &score)) {
|
||||
return ScoreResult();
|
||||
->RunScript(context, worklet_script_->Get(isolate), "scoreAd", args,
|
||||
error_msg_out)
|
||||
.ToLocal(&score_ad_result)) {
|
||||
return ScoreResult(std::move(error_msg_out));
|
||||
}
|
||||
|
||||
if (score <= 0 || std::isnan(score) || !std::isfinite(score))
|
||||
if (!gin::ConvertFromV8(isolate, score_ad_result, &score) ||
|
||||
std::isnan(score) || !std::isfinite(score)) {
|
||||
return ScoreResult(
|
||||
base::StrCat({script_source_url_.spec(),
|
||||
" scoreAd() did not return a valid number."}));
|
||||
}
|
||||
|
||||
if (score <= 0)
|
||||
return ScoreResult();
|
||||
|
||||
return ScoreResult(score);
|
||||
@@ -236,11 +266,12 @@ SellerWorklet::Report SellerWorklet::ReportResult(
|
||||
args.push_back(browser_signals);
|
||||
|
||||
v8::Local<v8::Value> signals_for_winner_value;
|
||||
base::Optional<std::string> error_msg_out;
|
||||
if (!v8_helper_
|
||||
->RunScript(context, worklet_script_->Get(isolate), "reportResult",
|
||||
args)
|
||||
args, error_msg_out)
|
||||
.ToLocal(&signals_for_winner_value)) {
|
||||
return Report();
|
||||
return Report(std::move(error_msg_out));
|
||||
}
|
||||
|
||||
// Consider lack of error but no return value type, or a return value that
|
||||
@@ -256,10 +287,12 @@ SellerWorklet::Report SellerWorklet::ReportResult(
|
||||
|
||||
void SellerWorklet::OnDownloadComplete(
|
||||
LoadWorkletCallback load_worklet_callback,
|
||||
std::unique_ptr<v8::Global<v8::UnboundScript>> worklet_script) {
|
||||
std::unique_ptr<v8::Global<v8::UnboundScript>> worklet_script,
|
||||
base::Optional<std::string> error_msg) {
|
||||
worklet_loader_.reset();
|
||||
worklet_script_ = std::move(worklet_script);
|
||||
std::move(load_worklet_callback).Run(worklet_script_ != nullptr);
|
||||
std::move(load_worklet_callback)
|
||||
.Run(worklet_script_ != nullptr, std::move(error_msg));
|
||||
}
|
||||
|
||||
} // namespace auction_worklet
|
||||
|
@@ -9,6 +9,7 @@
|
||||
#include <string>
|
||||
|
||||
#include "base/callback.h"
|
||||
#include "base/optional.h"
|
||||
#include "base/time/time.h"
|
||||
#include "content/services/auction_worklet/public/mojom/auction_worklet_service.mojom-forward.h"
|
||||
#include "services/network/public/mojom/url_loader_factory.mojom-forward.h"
|
||||
@@ -27,8 +28,7 @@ class WorkletLoader;
|
||||
// seller worklet's Javascript.
|
||||
class SellerWorklet {
|
||||
public:
|
||||
// The result of invoking scoreAd(). This could just be a double, but planning
|
||||
// to add error information down the line.
|
||||
// The result of invoking scoreAd().
|
||||
struct ScoreResult {
|
||||
// Creates a ScoreResult for a failure or rejected bid.
|
||||
ScoreResult();
|
||||
@@ -36,15 +36,28 @@ class SellerWorklet {
|
||||
// Creates a ScoreResult for a successfully scored ad. `score` will be > 0.
|
||||
explicit ScoreResult(double score);
|
||||
|
||||
// Creates a ScoreResult representing a fatal error, potentially with a
|
||||
// helpful diagnostic message in `error_msg`.
|
||||
explicit ScoreResult(base::Optional<std::string> error_msg);
|
||||
|
||||
ScoreResult(const ScoreResult& other);
|
||||
ScoreResult(ScoreResult&& other);
|
||||
|
||||
~ScoreResult();
|
||||
|
||||
ScoreResult& operator=(const ScoreResult&);
|
||||
ScoreResult& operator=(ScoreResult&&);
|
||||
|
||||
// `success` will be false on any type of failure, and score will be 0.
|
||||
//
|
||||
// TODO(mmenke): Pass along some sort of error string instead, to make
|
||||
// debugging easier.
|
||||
bool success = false;
|
||||
|
||||
// Score. 0 if no bid is offered (even if underlying script returned a
|
||||
// negative value).
|
||||
double score = 0;
|
||||
|
||||
// Error message for debugging. This is not guaranteed to be present on
|
||||
// failure.
|
||||
base::Optional<std::string> error_msg;
|
||||
};
|
||||
|
||||
// The result of invoking reportResult().
|
||||
@@ -55,11 +68,20 @@ class SellerWorklet {
|
||||
// Creates a Report for a successful call.
|
||||
Report(std::string signals_for_winner, GURL report_url);
|
||||
|
||||
// Creates a ScoreResult representing a fatal error, potentially with a
|
||||
// helpful diagnostic message in `error_msg`.
|
||||
explicit Report(base::Optional<std::string> error_msg);
|
||||
|
||||
Report(const Report& other);
|
||||
Report(Report&& other);
|
||||
|
||||
~Report();
|
||||
|
||||
Report& operator=(const Report&);
|
||||
Report& operator=(Report&&);
|
||||
|
||||
// `success` will be false on any type of failure, including lack of a
|
||||
// method.
|
||||
//
|
||||
// TODO(mmenke): Pass along some sort of error string instead, to make
|
||||
// debugging easier.
|
||||
bool success = false;
|
||||
|
||||
// JSON data as a string. Sent to the winner's ReportWin function. JSON
|
||||
@@ -69,9 +91,15 @@ class SellerWorklet {
|
||||
// Report URL, if one is provided. Empty on failure, or if no report URL is
|
||||
// provided.
|
||||
GURL report_url;
|
||||
|
||||
// Error message for debugging. This isn't guaranteed to have a value for
|
||||
// all failures.
|
||||
base::Optional<std::string> error_msg;
|
||||
};
|
||||
|
||||
using LoadWorkletCallback = base::OnceCallback<void(bool success)>;
|
||||
using LoadWorkletCallback =
|
||||
base::OnceCallback<void(bool success,
|
||||
base::Optional<std::string> error_msg)>;
|
||||
|
||||
// Starts loading the worklet script on construction. Callback will be invoked
|
||||
// asynchronously once the data has been fetched or an error has occurred.
|
||||
@@ -109,8 +137,10 @@ class SellerWorklet {
|
||||
private:
|
||||
void OnDownloadComplete(
|
||||
LoadWorkletCallback load_worklet_callback,
|
||||
std::unique_ptr<v8::Global<v8::UnboundScript>> worklet_script);
|
||||
std::unique_ptr<v8::Global<v8::UnboundScript>> worklet_script,
|
||||
base::Optional<std::string> error_msg);
|
||||
|
||||
const GURL script_source_url_;
|
||||
AuctionV8Helper* const v8_helper_;
|
||||
std::unique_ptr<WorkletLoader> worklet_loader_;
|
||||
|
||||
|
@@ -18,9 +18,13 @@
|
||||
#include "content/services/auction_worklet/worklet_test_util.h"
|
||||
#include "net/http/http_status_code.h"
|
||||
#include "services/network/test/test_url_loader_factory.h"
|
||||
#include "testing/gmock/include/gmock/gmock-matchers.h"
|
||||
#include "testing/gtest/include/gtest/gtest.h"
|
||||
#include "url/gurl.h"
|
||||
|
||||
using testing::HasSubstr;
|
||||
using testing::StartsWith;
|
||||
|
||||
namespace auction_worklet {
|
||||
namespace {
|
||||
|
||||
@@ -85,23 +89,29 @@ class SellerWorkletTest : public testing::Test {
|
||||
// return line, expecting the provided result.
|
||||
void RunScoreAdWithReturnValueExpectingResult(
|
||||
const std::string& raw_return_value,
|
||||
double expected_score) {
|
||||
double expected_score,
|
||||
base::Optional<std::string> expected_error_msg = base::nullopt) {
|
||||
RunScoreAdWithJavascriptExpectingResult(
|
||||
CreateScoreAdScript(raw_return_value), expected_score);
|
||||
CreateScoreAdScript(raw_return_value), expected_score,
|
||||
std::move(expected_error_msg));
|
||||
}
|
||||
|
||||
// Configures `url_loader_factory_` to return the provided script, and then
|
||||
// runs its generate_bid() function. Then runs the script, expecting the
|
||||
// provided result.
|
||||
void RunScoreAdWithJavascriptExpectingResult(const std::string& javascript,
|
||||
double expected_score) {
|
||||
void RunScoreAdWithJavascriptExpectingResult(
|
||||
const std::string& javascript,
|
||||
double expected_score,
|
||||
base::Optional<std::string> expected_error_msg = base::nullopt) {
|
||||
SCOPED_TRACE(javascript);
|
||||
AddJavascriptResponse(&url_loader_factory_, url_, javascript);
|
||||
RunScoreAdExpectingResult(expected_score);
|
||||
RunScoreAdExpectingResult(expected_score, std::move(expected_error_msg));
|
||||
}
|
||||
|
||||
// Loads and runs a scode_ad() script, expecting the supplied result.
|
||||
void RunScoreAdExpectingResult(double expected_score) {
|
||||
void RunScoreAdExpectingResult(
|
||||
double expected_score,
|
||||
base::Optional<std::string> expected_error_msg = base::nullopt) {
|
||||
auto seller_worket = CreateWorklet();
|
||||
ASSERT_TRUE(seller_worket);
|
||||
|
||||
@@ -113,6 +123,10 @@ class SellerWorkletTest : public testing::Test {
|
||||
browser_signal_bidding_duration_);
|
||||
EXPECT_EQ(expected_score > 0, actual_result.success);
|
||||
EXPECT_EQ(expected_score, actual_result.score);
|
||||
EXPECT_EQ(expected_error_msg.has_value(),
|
||||
actual_result.error_msg.has_value());
|
||||
EXPECT_EQ(expected_error_msg.value_or("Not an error"),
|
||||
actual_result.error_msg.value_or("Not an error"));
|
||||
}
|
||||
|
||||
// Configures `url_loader_factory_` to return a report_result() script created
|
||||
@@ -152,6 +166,10 @@ class SellerWorkletTest : public testing::Test {
|
||||
EXPECT_EQ(expected_report.signals_for_winner,
|
||||
actual_result.signals_for_winner);
|
||||
EXPECT_EQ(expected_report.report_url, actual_result.report_url);
|
||||
EXPECT_EQ(expected_report.error_msg.has_value(),
|
||||
actual_result.error_msg.has_value());
|
||||
EXPECT_EQ(expected_report.error_msg.value_or("Not an error"),
|
||||
actual_result.error_msg.value_or("Not an error"));
|
||||
}
|
||||
|
||||
// Create a SellerWorklet, waiting for the URLLoader to complete. Returns
|
||||
@@ -160,7 +178,7 @@ class SellerWorkletTest : public testing::Test {
|
||||
CHECK(!load_script_run_loop_);
|
||||
|
||||
create_worklet_succeeded_ = false;
|
||||
auto bidder_worket = std::make_unique<SellerWorklet>(
|
||||
auto seller_worket = std::make_unique<SellerWorklet>(
|
||||
&url_loader_factory_, url_, &v8_helper_,
|
||||
base::BindOnce(&SellerWorkletTest::CreateWorkletCallback,
|
||||
base::Unretained(this)));
|
||||
@@ -169,15 +187,21 @@ class SellerWorkletTest : public testing::Test {
|
||||
load_script_run_loop_.reset();
|
||||
if (!create_worklet_succeeded_)
|
||||
return nullptr;
|
||||
return bidder_worket;
|
||||
return seller_worket;
|
||||
}
|
||||
|
||||
protected:
|
||||
void CreateWorkletCallback(bool success) {
|
||||
void CreateWorkletCallback(bool success,
|
||||
base::Optional<std::string> error_msg) {
|
||||
create_worklet_succeeded_ = success;
|
||||
error_msg_ = std::move(error_msg);
|
||||
if (success)
|
||||
EXPECT_FALSE(error_msg_.has_value());
|
||||
load_script_run_loop_->Quit();
|
||||
}
|
||||
|
||||
std::string last_error_msg() { return error_msg_.value_or("Not an error"); }
|
||||
|
||||
base::test::TaskEnvironment task_environment_;
|
||||
|
||||
const GURL url_ = GURL("https://url.test/");
|
||||
@@ -201,6 +225,7 @@ class SellerWorkletTest : public testing::Test {
|
||||
// synchronously.
|
||||
std::unique_ptr<base::RunLoop> load_script_run_loop_;
|
||||
bool create_worklet_succeeded_ = false;
|
||||
base::Optional<std::string> error_msg_;
|
||||
|
||||
network::TestURLLoaderFactory url_loader_factory_;
|
||||
AuctionV8Helper v8_helper_;
|
||||
@@ -210,11 +235,15 @@ TEST_F(SellerWorkletTest, NetworkError) {
|
||||
url_loader_factory_.AddResponse(url_.spec(), CreateBasicSellAdScript(),
|
||||
net::HTTP_NOT_FOUND);
|
||||
EXPECT_FALSE(CreateWorklet());
|
||||
EXPECT_EQ("Failed to load https://url.test/ HTTP status = 404 Not Found.",
|
||||
last_error_msg());
|
||||
}
|
||||
|
||||
TEST_F(SellerWorkletTest, CompileError) {
|
||||
AddJavascriptResponse(&url_loader_factory_, url_, "Invalid Javascript");
|
||||
EXPECT_FALSE(CreateWorklet());
|
||||
EXPECT_THAT(last_error_msg(), StartsWith("https://url.test/:1 "));
|
||||
EXPECT_THAT(last_error_msg(), HasSubstr("SyntaxError"));
|
||||
}
|
||||
|
||||
// Test parsing of return values.
|
||||
@@ -229,21 +258,31 @@ TEST_F(SellerWorkletTest, ScoreAd) {
|
||||
RunScoreAdWithReturnValueExpectingResult("-10", 0);
|
||||
|
||||
// No return value.
|
||||
RunScoreAdWithReturnValueExpectingResult("", 0);
|
||||
RunScoreAdWithReturnValueExpectingResult(
|
||||
"", 0, "https://url.test/ scoreAd() did not return a valid number.");
|
||||
|
||||
// Wrong return type / invalid values.
|
||||
RunScoreAdWithReturnValueExpectingResult("[15]", 0);
|
||||
RunScoreAdWithReturnValueExpectingResult("1/0", 0);
|
||||
RunScoreAdWithReturnValueExpectingResult("0/0", 0);
|
||||
RunScoreAdWithReturnValueExpectingResult("-1/0", 0);
|
||||
RunScoreAdWithReturnValueExpectingResult("true", 0);
|
||||
RunScoreAdWithReturnValueExpectingResult(
|
||||
"[15]", 0, "https://url.test/ scoreAd() did not return a valid number.");
|
||||
RunScoreAdWithReturnValueExpectingResult(
|
||||
"1/0", 0, "https://url.test/ scoreAd() did not return a valid number.");
|
||||
RunScoreAdWithReturnValueExpectingResult(
|
||||
"0/0", 0, "https://url.test/ scoreAd() did not return a valid number.");
|
||||
RunScoreAdWithReturnValueExpectingResult(
|
||||
"-1/0", 0, "https://url.test/ scoreAd() did not return a valid number.");
|
||||
RunScoreAdWithReturnValueExpectingResult(
|
||||
"true", 0, "https://url.test/ scoreAd() did not return a valid number.");
|
||||
|
||||
// Throw exception.
|
||||
RunScoreAdWithReturnValueExpectingResult("shrimp", 0);
|
||||
RunScoreAdWithReturnValueExpectingResult(
|
||||
"shrimp", 0,
|
||||
"https://url.test/:4 Uncaught ReferenceError: shrimp is not defined.");
|
||||
}
|
||||
|
||||
TEST_F(SellerWorkletTest, ScoreAdDateNotAvailable) {
|
||||
RunScoreAdWithReturnValueExpectingResult("Date.parse(Date().toString())", 0);
|
||||
RunScoreAdWithReturnValueExpectingResult(
|
||||
"Date.parse(Date().toString())", 0,
|
||||
"https://url.test/:4 Uncaught ReferenceError: Date is not defined.");
|
||||
}
|
||||
|
||||
// Checks that input parameters are correctly passed in.
|
||||
@@ -363,7 +402,9 @@ TEST_F(SellerWorkletTest, ReportResult) {
|
||||
|
||||
// Throw exception.
|
||||
RunReportResultCreatedScriptExpectingResult(
|
||||
"shrimp", std::string() /* extra_code */, SellerWorklet::Report());
|
||||
"shrimp", std::string() /* extra_code */,
|
||||
SellerWorklet::Report("https://url.test/:4 Uncaught ReferenceError: "
|
||||
"shrimp is not defined."));
|
||||
}
|
||||
|
||||
// Tests reporting URLs.
|
||||
@@ -377,29 +418,49 @@ TEST_F(SellerWorkletTest, ReportResultSendReportTo) {
|
||||
|
||||
// Disallowed schemes.
|
||||
RunReportResultCreatedScriptExpectingResult(
|
||||
"1", R"(sendReportTo("http://foo.test/"))", SellerWorklet::Report());
|
||||
"1", R"(sendReportTo("http://foo.test/"))",
|
||||
SellerWorklet::Report("https://url.test/:3 Uncaught TypeError: "
|
||||
"sendReportTo must be passed a valid HTTPS url."));
|
||||
RunReportResultCreatedScriptExpectingResult(
|
||||
"1", R"(sendReportTo("file:///foo/"))", SellerWorklet::Report());
|
||||
"1", R"(sendReportTo("file:///foo/"))",
|
||||
SellerWorklet::Report("https://url.test/:3 Uncaught TypeError: "
|
||||
"sendReportTo must be passed a valid HTTPS url."));
|
||||
|
||||
// Multiple calls.
|
||||
RunReportResultCreatedScriptExpectingResult(
|
||||
"1",
|
||||
R"(sendReportTo("https://foo.test/"); sendReportTo("https://foo.test/"))",
|
||||
SellerWorklet::Report());
|
||||
SellerWorklet::Report("https://url.test/:3 Uncaught TypeError: "
|
||||
"sendReportTo may be called at most once."));
|
||||
|
||||
// No message if caught, but still no URL.
|
||||
RunReportResultCreatedScriptExpectingResult(
|
||||
"1",
|
||||
R"(try {
|
||||
sendReportTo("https://foo.test/");
|
||||
sendReportTo("https://foo.test/")} catch(e) {})",
|
||||
SellerWorklet::Report("1", GURL()));
|
||||
|
||||
// Not a URL.
|
||||
RunReportResultCreatedScriptExpectingResult("1", R"(sendReportTo("France"))",
|
||||
SellerWorklet::Report());
|
||||
RunReportResultCreatedScriptExpectingResult("1", R"(sendReportTo(null))",
|
||||
SellerWorklet::Report());
|
||||
RunReportResultCreatedScriptExpectingResult("1", R"(sendReportTo([5]))",
|
||||
SellerWorklet::Report());
|
||||
RunReportResultCreatedScriptExpectingResult(
|
||||
"1", R"(sendReportTo("France"))",
|
||||
SellerWorklet::Report("https://url.test/:3 Uncaught TypeError: "
|
||||
"sendReportTo must be passed a valid HTTPS url."));
|
||||
RunReportResultCreatedScriptExpectingResult(
|
||||
"1", R"(sendReportTo(null))",
|
||||
SellerWorklet::Report("https://url.test/:3 Uncaught TypeError: "
|
||||
"sendReportTo requires 1 string parameter."));
|
||||
RunReportResultCreatedScriptExpectingResult(
|
||||
"1", R"(sendReportTo([5]))",
|
||||
SellerWorklet::Report("https://url.test/:3 Uncaught TypeError: "
|
||||
"sendReportTo requires 1 string parameter."));
|
||||
}
|
||||
|
||||
TEST_F(SellerWorkletTest, ReportResultDateNotAvailable) {
|
||||
RunReportResultCreatedScriptExpectingResult(
|
||||
"1", R"(sendReportTo("https://foo.test/" + Date().toString()))",
|
||||
SellerWorklet::Report());
|
||||
SellerWorklet::Report(
|
||||
"https://url.test/:3 Uncaught ReferenceError: Date is not defined."));
|
||||
}
|
||||
|
||||
TEST_F(SellerWorkletTest, ReportResultParameters) {
|
||||
|
@@ -11,6 +11,7 @@
|
||||
#include "base/bind.h"
|
||||
#include "base/callback.h"
|
||||
#include "base/logging.h"
|
||||
#include "base/strings/strcat.h"
|
||||
#include "content/services/auction_worklet/auction_downloader.h"
|
||||
#include "content/services/auction_worklet/auction_v8_helper.h"
|
||||
#include "gin/converter.h"
|
||||
@@ -28,7 +29,8 @@ TrustedBiddingSignals::TrustedBiddingSignals(
|
||||
const GURL& trusted_bidding_signals_url,
|
||||
AuctionV8Helper* v8_helper,
|
||||
LoadSignalsCallback load_signals_callback)
|
||||
: v8_helper_(v8_helper),
|
||||
: trusted_bidding_signals_url_(trusted_bidding_signals_url),
|
||||
v8_helper_(v8_helper),
|
||||
load_signals_callback_(std::move(load_signals_callback)) {
|
||||
DCHECK(!trusted_bidding_signals_keys.empty());
|
||||
DCHECK(load_signals_callback_);
|
||||
@@ -81,14 +83,17 @@ v8::Local<v8::Object> TrustedBiddingSignals::GetSignals(
|
||||
|
||||
void TrustedBiddingSignals::OnDownloadComplete(
|
||||
std::vector<std::string> trusted_bidding_signals_keys,
|
||||
std::unique_ptr<std::string> body) {
|
||||
std::unique_ptr<std::string> body,
|
||||
base::Optional<std::string> error_msg) {
|
||||
auction_downloader_.reset();
|
||||
|
||||
if (!body) {
|
||||
std::move(load_signals_callback_).Run(false);
|
||||
std::move(load_signals_callback_).Run(false, std::move(error_msg));
|
||||
return;
|
||||
}
|
||||
|
||||
DCHECK(!error_msg.has_value());
|
||||
|
||||
AuctionV8Helper::FullIsolateScope isolate_scope(v8_helper_);
|
||||
v8::Context::Scope context_scope(v8_helper_->scratch_context());
|
||||
|
||||
@@ -96,7 +101,9 @@ void TrustedBiddingSignals::OnDownloadComplete(
|
||||
if (!v8_helper_->CreateValueFromJson(v8_helper_->scratch_context(), *body)
|
||||
.ToLocal(&v8_data) ||
|
||||
!v8_data->IsObject()) {
|
||||
std::move(load_signals_callback_).Run(false);
|
||||
std::string error = base::StrCat({trusted_bidding_signals_url_.spec(),
|
||||
" Unable to parse as a JSON object."});
|
||||
std::move(load_signals_callback_).Run(false, std::move(error));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -108,7 +115,7 @@ void TrustedBiddingSignals::OnDownloadComplete(
|
||||
v8::Local<v8::Value> v8_string_value;
|
||||
std::string value;
|
||||
if (!v8_helper_->CreateUtf8String(key).ToLocal(&v8_key)) {
|
||||
std::move(load_signals_callback_).Run(false);
|
||||
std::move(load_signals_callback_).Run(false, base::nullopt);
|
||||
return;
|
||||
}
|
||||
// Only the `has_result` check should be able to fail.
|
||||
@@ -124,7 +131,7 @@ void TrustedBiddingSignals::OnDownloadComplete(
|
||||
}
|
||||
json_data_[key] = std::move(value);
|
||||
}
|
||||
std::move(load_signals_callback_).Run(true);
|
||||
std::move(load_signals_callback_).Run(true, base::nullopt);
|
||||
}
|
||||
|
||||
} // namespace auction_worklet
|
||||
|
@@ -11,6 +11,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "base/callback.h"
|
||||
#include "base/optional.h"
|
||||
#include "services/network/public/mojom/url_loader_factory.mojom-forward.h"
|
||||
#include "url/gurl.h"
|
||||
#include "v8/include/v8.h"
|
||||
@@ -33,7 +34,9 @@ class AuctionV8Helper;
|
||||
// clone operation as a copy).
|
||||
class TrustedBiddingSignals {
|
||||
public:
|
||||
using LoadSignalsCallback = base::OnceCallback<void(bool success)>;
|
||||
using LoadSignalsCallback =
|
||||
base::OnceCallback<void(bool success,
|
||||
base::Optional<std::string> error_msg)>;
|
||||
|
||||
// Starts loading the JSON data on construction. `trusted_bidding_signals_url`
|
||||
// must be the base URL (no query params added). Callback will be invoked
|
||||
@@ -62,8 +65,10 @@ class TrustedBiddingSignals {
|
||||
|
||||
private:
|
||||
void OnDownloadComplete(std::vector<std::string> trusted_bidding_signals_keys,
|
||||
std::unique_ptr<std::string> body);
|
||||
std::unique_ptr<std::string> body,
|
||||
base::Optional<std::string> error_msg);
|
||||
|
||||
const GURL trusted_bidding_signals_url_; // original, for error messages.
|
||||
AuctionV8Helper* const v8_helper_;
|
||||
|
||||
LoadSignalsCallback load_signals_callback_;
|
||||
|
@@ -98,8 +98,11 @@ class TrustedBiddingSignalsTest : public testing::Test {
|
||||
}
|
||||
|
||||
protected:
|
||||
void LoadSignalsCallback(bool success) {
|
||||
void LoadSignalsCallback(bool success,
|
||||
base::Optional<std::string> error_msg) {
|
||||
load_signals_succeeded_ = success;
|
||||
error_msg_ = std::move(error_msg);
|
||||
EXPECT_EQ(load_signals_succeeded_, !error_msg_.has_value());
|
||||
load_signals_run_loop_->Quit();
|
||||
}
|
||||
|
||||
@@ -113,6 +116,7 @@ class TrustedBiddingSignalsTest : public testing::Test {
|
||||
// synchronously.
|
||||
std::unique_ptr<base::RunLoop> load_signals_run_loop_;
|
||||
bool load_signals_succeeded_ = false;
|
||||
base::Optional<std::string> error_msg_;
|
||||
|
||||
network::TestURLLoaderFactory url_loader_factory_;
|
||||
AuctionV8Helper v8_helper_;
|
||||
@@ -123,18 +127,29 @@ TEST_F(TrustedBiddingSignalsTest, NetworkError) {
|
||||
"https://url.test/?hostname=publisher&keys=key1", kBaseJson,
|
||||
net::HTTP_NOT_FOUND);
|
||||
EXPECT_FALSE(FetchBiddingSignals({"key1"}, kHostname));
|
||||
ASSERT_TRUE(error_msg_.has_value());
|
||||
EXPECT_EQ(
|
||||
"Failed to load https://url.test/?hostname=publisher&keys=key1 "
|
||||
"HTTP status = 404 Not Found.",
|
||||
error_msg_.value());
|
||||
}
|
||||
|
||||
TEST_F(TrustedBiddingSignalsTest, ResponseNotJson) {
|
||||
EXPECT_FALSE(FetchBiddingSignalsWithResponse(
|
||||
GURL("https://url.test/?hostname=publisher&keys=key1"), "Not Json",
|
||||
{"key1"}, kHostname));
|
||||
ASSERT_TRUE(error_msg_.has_value());
|
||||
EXPECT_EQ("https://url.test/ Unable to parse as a JSON object.",
|
||||
error_msg_.value());
|
||||
}
|
||||
|
||||
TEST_F(TrustedBiddingSignalsTest, ResponseNotObject) {
|
||||
EXPECT_FALSE(FetchBiddingSignalsWithResponse(
|
||||
GURL("https://url.test/?hostname=publisher&keys=key1"), "42", {"key1"},
|
||||
kHostname));
|
||||
ASSERT_TRUE(error_msg_.has_value());
|
||||
EXPECT_EQ("https://url.test/ Unable to parse as a JSON object.",
|
||||
error_msg_.value());
|
||||
}
|
||||
|
||||
TEST_F(TrustedBiddingSignalsTest, KeyMissing) {
|
||||
|
@@ -38,17 +38,20 @@ WorkletLoader::WorkletLoader(
|
||||
|
||||
WorkletLoader::~WorkletLoader() = default;
|
||||
|
||||
void WorkletLoader::OnDownloadComplete(std::unique_ptr<std::string> body) {
|
||||
void WorkletLoader::OnDownloadComplete(std::unique_ptr<std::string> body,
|
||||
base::Optional<std::string> error_msg) {
|
||||
DCHECK(load_worklet_callback_);
|
||||
|
||||
auction_downloader_.reset();
|
||||
|
||||
if (!body) {
|
||||
std::move(load_worklet_callback_).Run(nullptr /* worklet_script */);
|
||||
std::move(load_worklet_callback_)
|
||||
.Run(nullptr /* worklet_script */, std::move(error_msg));
|
||||
return;
|
||||
}
|
||||
|
||||
std::unique_ptr<v8::Global<v8::UnboundScript>> global_script;
|
||||
DCHECK(!error_msg.has_value());
|
||||
|
||||
// Need to release the isolate and context before invoking the callback, in
|
||||
// case the `v8_helper_` is destroyed.
|
||||
@@ -57,13 +60,15 @@ void WorkletLoader::OnDownloadComplete(std::unique_ptr<std::string> body) {
|
||||
v8::Context::Scope context_scope(v8_helper_->scratch_context());
|
||||
|
||||
v8::Local<v8::UnboundScript> local_script;
|
||||
if (v8_helper_->Compile(*body, script_source_url_).ToLocal(&local_script)) {
|
||||
if (v8_helper_->Compile(*body, script_source_url_, error_msg)
|
||||
.ToLocal(&local_script)) {
|
||||
global_script = std::make_unique<v8::Global<v8::UnboundScript>>(
|
||||
v8_helper_->isolate(), local_script);
|
||||
}
|
||||
}
|
||||
|
||||
std::move(load_worklet_callback_).Run(std::move(global_script));
|
||||
std::move(load_worklet_callback_)
|
||||
.Run(std::move(global_script), std::move(error_msg));
|
||||
}
|
||||
|
||||
} // namespace auction_worklet
|
||||
|
@@ -10,6 +10,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "base/callback.h"
|
||||
#include "base/optional.h"
|
||||
#include "services/network/public/mojom/url_loader_factory.mojom-forward.h"
|
||||
#include "url/gurl.h"
|
||||
#include "v8/include/v8.h"
|
||||
@@ -25,11 +26,9 @@ class WorkletLoader {
|
||||
// On success, `worklet_script` is compiled script, not bound to any context.
|
||||
// It can be repeatedly bound to different contexts and executed, without
|
||||
// persisting any state.
|
||||
//
|
||||
// TODO(mmenke): Pass along some sort of error string on failure, to make
|
||||
// debugging easier.
|
||||
using LoadWorkletCallback = base::OnceCallback<void(
|
||||
std::unique_ptr<v8::Global<v8::UnboundScript>> worklet_script)>;
|
||||
std::unique_ptr<v8::Global<v8::UnboundScript>> worklet_script,
|
||||
base::Optional<std::string> error_msg)>;
|
||||
|
||||
// Starts loading the worklet script on construction. Callback will be invoked
|
||||
// asynchronously once the data has been fetched or an error has occurred.
|
||||
@@ -43,7 +42,8 @@ class WorkletLoader {
|
||||
~WorkletLoader();
|
||||
|
||||
private:
|
||||
void OnDownloadComplete(std::unique_ptr<std::string> body);
|
||||
void OnDownloadComplete(std::unique_ptr<std::string> body,
|
||||
base::Optional<std::string> error_msg);
|
||||
|
||||
const GURL script_source_url_;
|
||||
AuctionV8Helper* const v8_helper_;
|
||||
|
@@ -16,10 +16,14 @@
|
||||
#include "content/services/auction_worklet/worklet_test_util.h"
|
||||
#include "net/http/http_status_code.h"
|
||||
#include "services/network/test/test_url_loader_factory.h"
|
||||
#include "testing/gmock/include/gmock/gmock-matchers.h"
|
||||
#include "testing/gtest/include/gtest/gtest.h"
|
||||
#include "url/gurl.h"
|
||||
#include "v8/include/v8.h"
|
||||
|
||||
using testing::HasSubstr;
|
||||
using testing::StartsWith;
|
||||
|
||||
namespace auction_worklet {
|
||||
namespace {
|
||||
|
||||
@@ -34,11 +38,18 @@ class WorkletLoaderTest : public testing::Test {
|
||||
~WorkletLoaderTest() override = default;
|
||||
|
||||
void LoadWorkletCallback(
|
||||
std::unique_ptr<v8::Global<v8::UnboundScript>> worklet_script) {
|
||||
std::unique_ptr<v8::Global<v8::UnboundScript>> worklet_script,
|
||||
base::Optional<std::string> error_msg) {
|
||||
load_succeeded_ = !!worklet_script;
|
||||
error_msg_ = std::move(error_msg);
|
||||
EXPECT_EQ(load_succeeded_, !error_msg_.has_value());
|
||||
run_loop_.Quit();
|
||||
}
|
||||
|
||||
std::string last_error_msg() const {
|
||||
return error_msg_.value_or("Not an error");
|
||||
}
|
||||
|
||||
protected:
|
||||
base::test::TaskEnvironment task_environment_;
|
||||
|
||||
@@ -47,6 +58,7 @@ class WorkletLoaderTest : public testing::Test {
|
||||
GURL url_ = GURL("https://foo.test/");
|
||||
base::RunLoop run_loop_;
|
||||
bool load_succeeded_ = false;
|
||||
base::Optional<std::string> error_msg_;
|
||||
};
|
||||
|
||||
TEST_F(WorkletLoaderTest, NetworkError) {
|
||||
@@ -59,6 +71,8 @@ TEST_F(WorkletLoaderTest, NetworkError) {
|
||||
base::Unretained(this)));
|
||||
run_loop_.Run();
|
||||
EXPECT_FALSE(load_succeeded_);
|
||||
EXPECT_EQ("Failed to load https://foo.test/ HTTP status = 404 Not Found.",
|
||||
last_error_msg());
|
||||
}
|
||||
|
||||
TEST_F(WorkletLoaderTest, CompileError) {
|
||||
@@ -69,6 +83,8 @@ TEST_F(WorkletLoaderTest, CompileError) {
|
||||
base::Unretained(this)));
|
||||
run_loop_.Run();
|
||||
EXPECT_FALSE(load_succeeded_);
|
||||
EXPECT_THAT(last_error_msg(), StartsWith("https://foo.test/:1 "));
|
||||
EXPECT_THAT(last_error_msg(), HasSubstr("SyntaxError"));
|
||||
}
|
||||
|
||||
TEST_F(WorkletLoaderTest, Success) {
|
||||
@@ -92,9 +108,10 @@ TEST_F(WorkletLoaderTest, DeleteDuringCallbackSuccess) {
|
||||
std::make_unique<WorkletLoader>(
|
||||
&url_loader_factory_, url_, v8_helper.get(),
|
||||
base::BindLambdaForTesting(
|
||||
[&](std::unique_ptr<v8::Global<v8::UnboundScript>>
|
||||
worklet_script) {
|
||||
[&](std::unique_ptr<v8::Global<v8::UnboundScript>> worklet_script,
|
||||
base::Optional<std::string> error_msg) {
|
||||
EXPECT_TRUE(worklet_script);
|
||||
EXPECT_FALSE(error_msg.has_value());
|
||||
worklet_script.reset();
|
||||
worklet_loader.reset();
|
||||
v8_helper.reset();
|
||||
@@ -114,9 +131,13 @@ TEST_F(WorkletLoaderTest, DeleteDuringCallbackCompileError) {
|
||||
std::make_unique<WorkletLoader>(
|
||||
&url_loader_factory_, url_, v8_helper.get(),
|
||||
base::BindLambdaForTesting(
|
||||
[&](std::unique_ptr<v8::Global<v8::UnboundScript>>
|
||||
worklet_script) {
|
||||
[&](std::unique_ptr<v8::Global<v8::UnboundScript>> worklet_script,
|
||||
base::Optional<std::string> error_msg) {
|
||||
EXPECT_FALSE(worklet_script);
|
||||
ASSERT_TRUE(error_msg.has_value());
|
||||
EXPECT_THAT(error_msg.value(),
|
||||
StartsWith("https://foo.test/:1 "));
|
||||
EXPECT_THAT(error_msg.value(), HasSubstr("SyntaxError"));
|
||||
worklet_loader.reset();
|
||||
v8_helper.reset();
|
||||
run_loop.Quit();
|
||||
|
Reference in New Issue
Block a user