summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/fledge/tentative
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /testing/web-platform/tests/fledge/tentative
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/fledge/tentative')
-rw-r--r--testing/web-platform/tests/fledge/tentative/TODO90
-rw-r--r--testing/web-platform/tests/fledge/tentative/abort.https.window.js103
-rw-r--r--testing/web-platform/tests/fledge/tentative/auction-config-passed-to-worklets.https.window.js208
-rw-r--r--testing/web-platform/tests/fledge/tentative/auction-config.https.window.js392
-rw-r--r--testing/web-platform/tests/fledge/tentative/clear-origin-joined-ad-interest-groups.https.window.js312
-rw-r--r--testing/web-platform/tests/fledge/tentative/component-ads.https.window.js449
-rw-r--r--testing/web-platform/tests/fledge/tentative/component-auction.https.window.js719
-rw-r--r--testing/web-platform/tests/fledge/tentative/cross-origin.https.window.js452
-rw-r--r--testing/web-platform/tests/fledge/tentative/currency.https.window.js874
-rw-r--r--testing/web-platform/tests/fledge/tentative/direct-from-seller-signals.https.window.js616
-rw-r--r--testing/web-platform/tests/fledge/tentative/fetch-ad-auction-headers-insecure-context.tentative.http.html11
-rw-r--r--testing/web-platform/tests/fledge/tentative/fetch-ad-auction-headers.tentative.https.html12
-rw-r--r--testing/web-platform/tests/fledge/tentative/generate-bid-recency.https.window.js34
-rw-r--r--testing/web-platform/tests/fledge/tentative/insecure-context.window.js8
-rw-r--r--testing/web-platform/tests/fledge/tentative/interest-group-passed-to-generate-bid.https.window.js737
-rw-r--r--testing/web-platform/tests/fledge/tentative/join-leave-ad-interest-group-in-fenced-frame.https.window.js369
-rw-r--r--testing/web-platform/tests/fledge/tentative/join-leave-ad-interest-group.https.window.js604
-rw-r--r--testing/web-platform/tests/fledge/tentative/kanon-status-below-threshold.https.window.js20
-rw-r--r--testing/web-platform/tests/fledge/tentative/kanon-status-not-calculated.https.window.js20
-rw-r--r--testing/web-platform/tests/fledge/tentative/network.https.window.js327
-rw-r--r--testing/web-platform/tests/fledge/tentative/no-winner.https.window.js106
-rw-r--r--testing/web-platform/tests/fledge/tentative/register-ad-beacon.https.window.js307
-rw-r--r--testing/web-platform/tests/fledge/tentative/reporting-arguments.https.window.js305
-rw-r--r--testing/web-platform/tests/fledge/tentative/resources/bidding-logic.sub.py71
-rw-r--r--testing/web-platform/tests/fledge/tentative/resources/decision-logic.sub.py59
-rw-r--r--testing/web-platform/tests/fledge/tentative/resources/direct-from-seller-signals.py144
-rw-r--r--testing/web-platform/tests/fledge/tentative/resources/empty.html1
-rw-r--r--testing/web-platform/tests/fledge/tentative/resources/fenced-frame.sub.py26
-rw-r--r--testing/web-platform/tests/fledge/tentative/resources/fledge-util.sub.js645
-rw-r--r--testing/web-platform/tests/fledge/tentative/resources/fledge_http_server_util.py67
-rw-r--r--testing/web-platform/tests/fledge/tentative/resources/incrementer.wasmbin0 -> 46 bytes
-rw-r--r--testing/web-platform/tests/fledge/tentative/resources/redirect-to-trusted-signals.py22
-rw-r--r--testing/web-platform/tests/fledge/tentative/resources/redirect.py8
-rw-r--r--testing/web-platform/tests/fledge/tentative/resources/request-tracker.py113
-rw-r--r--testing/web-platform/tests/fledge/tentative/resources/set-cookie.asis3
-rw-r--r--testing/web-platform/tests/fledge/tentative/resources/subordinate-frame.sub.html89
-rw-r--r--testing/web-platform/tests/fledge/tentative/resources/subordinate-frame.sub.html.headers1
-rw-r--r--testing/web-platform/tests/fledge/tentative/resources/trusted-bidding-signals.py133
-rw-r--r--testing/web-platform/tests/fledge/tentative/resources/trusted-scoring-signals.py144
-rw-r--r--testing/web-platform/tests/fledge/tentative/resources/wasm-helper.py38
-rw-r--r--testing/web-platform/tests/fledge/tentative/resources/worklet-helpers.js23
-rw-r--r--testing/web-platform/tests/fledge/tentative/round-a-value.https.window.js161
-rw-r--r--testing/web-platform/tests/fledge/tentative/send-report-to.https.window.js155
-rw-r--r--testing/web-platform/tests/fledge/tentative/tie.https.window.js125
-rw-r--r--testing/web-platform/tests/fledge/tentative/trusted-bidding-signals.https.window.js787
-rw-r--r--testing/web-platform/tests/fledge/tentative/trusted-scoring-signals.https.window.js512
46 files changed, 10402 insertions, 0 deletions
diff --git a/testing/web-platform/tests/fledge/tentative/TODO b/testing/web-platform/tests/fledge/tentative/TODO
new file mode 100644
index 0000000000..0f68a7c914
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/TODO
@@ -0,0 +1,90 @@
+Need tests for (likely not a complete list):
+
+* Test how InterestGroup values affected by k-anon checks are passed to generateBid.
+ * adSizes, sizeGroups, ads, and adComponents all need such tests.
+ * adSizes and sizeGroups currently have no tests, since they are incorectly
+ currently not passed to generateBid at all.
+* Test empty ads array:
+ Maybe simplest to test its numBids is empty? Hard to test a script isn't run.
+* directFromSellerSignals.
+ * The expected order when both responses use the same ad slot is currently
+ undefined. However, we are in the process of resolving this matter by
+ implementing a LIFO approach, as outlined in progress at
+ crrev.com/c/4930438. Once this solution is in place, a test case will be
+ created by fetching two different URLs with signals that share the same
+ ad slot.
+ * After adding new test cases for the component auction, test the
+ directFromSellerSignals function with component auctions. Consider to
+ set up one auction in the top frame and two component auctions. Send
+ three fetch requests to retrieve three different AdAuctionSignals
+ headers. Ensure that you use three different seller origins for the
+ auctions and a different one for the buyer origin.
+* All generateBid() and scoreAd() input parameters.
+* All interest group fields (passed to auction, have effect on auction).
+ Very few fields covered.
+ Should be sure to cover buyerAndSellerReportingId and buyerReportingId for
+ component ads, as they should not be settable.
+ Already covered:
+ Validation when joining/leaving interest group.
+ renderURL and metadata for component ads (only integers covered, but
+ probably not worth covering all types, if we have coverage for the
+ main renderURL).
+* Filtering/prioritization (including bidding signals influencing priorities)
+* Size restrictions / ad and component ad sizes.
+* additionalBids.
+* adCost.
+* bidCurrency.
+* modellingSignals.
+* Updates (both after auction and triggered).
+* All auctionConfig parameters (including invalid auctionConfigs, and ones
+ with no buyers).
+* Joining interest group with duration of 0 should just leave the IG (not
+ currently guaranteed to work, due to potential time skew between processes).
+* Multiple buyers.
+* Multiple interest groups with same owner.
+* Multiple frame tests (including loading component
+ ad URNs in fenced frames of other frames, etc)
+* adAuctionConfig passed to reportResult().
+* Component auctions.
+ * Including cross-origin sellers.
+* browserSignals fields in scoring/bidding methods.
+* In reporting methods, browserSignals fields: topLevelSeller,
+ componentSeller, modifiedBid, adCost, madeHighestScoringOtherBid
+ (with interest group from another origin).
+* Loading ads in iframes.
+* In fencedframes window.fence.setReportEventDataForAutomaticBeacons()
+* Automatic beacons (e.g., reserved.top_navigation)
+* Network timeouts.
+* Validate specific escaping behavior logic (still under discussion). There
+ are a number of different rules for which characters are escaped, and
+ whether spaces are escaped as "%20" or "+".
+* Reports not sent if ad not used.
+* Interactions with local network access API, which requires public
+ networks to send CORS preflights for requests made over local networks.
+ Needs testing with public publisher pages running auctions with
+ sellers / buyers / update URLs on local networks.
+* Calling FLEDGE APIs (or at least leaveAdInterestGroup() with no args)
+ in a non-ad FencedFrame.
+* Promise AuctionConfig parameters
+* Test network requests in terms of fetch behavior
+ * Network partition (not yet specced).
+* Test that await is not needed for same-origin interest groups
+ * Verify that's still in the spec/explainer first.
+* executionMode
+ * Including cross-origin join/leave behavior with "group-by-origin" mode.
+* Make sure state is not shared.
+ * Across scoreAd() / generateBid() calls, and with report calls.
+ * In "group-by-origin" execution mode across IGs joined from different
+ origins, and between generateBid() and reportWin().
+* Test Content-Type headers allowed in responess for script/wasm/JSON fetches.
+* Test WASM support, updating createBiddingWasmHelperURL().
+
+If possible:
+* Aggregate reporting.
+* Join/leave permission delegation via .well-known files
+ * Including tests for clearOriginJoinedInterestGroups().
+ * Include tests for HTTP-y/fetch-y things (e.g., whether they have cookies)
+* k-anonymity.
+* Signals request batching. This is an optional feature, so can't require it,
+ but maybe a test where batching could be used, and make sure things work,
+ whether batching is used or not?
diff --git a/testing/web-platform/tests/fledge/tentative/abort.https.window.js b/testing/web-platform/tests/fledge/tentative/abort.https.window.js
new file mode 100644
index 0000000000..b99d60dd52
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/abort.https.window.js
@@ -0,0 +1,103 @@
+// META: script=/resources/testdriver.js
+// META: script=/common/utils.js
+// META: script=resources/fledge-util.sub.js
+// META: timeout=long
+
+"use strict;"
+
+promise_test(async test => {
+ const uuid = generateUuid(test);
+
+ // To minimize the risk of the auction completing before the abort signal,
+ // make the bid script hand, and increase the per-buyer script timeout.
+ await joinInterestGroup(
+ test, uuid,
+ createBiddingScriptURL({generateBid: 'while(1);'}));
+
+ let abortController = new AbortController();
+ let promise = runBasicFledgeAuction(
+ test, uuid,
+ { signal: abortController.signal,
+ perBuyerTimeouts: {'*': 1000}
+ });
+ abortController.abort('reason');
+ try {
+ await promise;
+ } catch (e) {
+ assert_equals(e, 'reason');
+ return;
+ }
+ throw 'Exception unexpectedly not thrown';
+}, 'Abort auction.');
+
+promise_test(async test => {
+ const uuid = generateUuid(test);
+
+ await joinInterestGroup(test, uuid);
+
+ let abortController = new AbortController();
+ abortController.abort('reason');
+ try {
+ await runBasicFledgeAuction(test, uuid, {signal: abortController.signal});
+ } catch (e) {
+ assert_equals(e, 'reason');
+ return;
+ }
+ throw 'Exception unexpectedly not thrown';
+}, 'Abort triggered before auction started.');
+
+promise_test(async test => {
+ const uuid = generateUuid(test);
+
+ // This doesn't have the header to be loaded in a fenced frame, but can still
+ // check that it was requested, which is all this test needs.
+ let trackingRenderURL =
+ createTrackerURL(origin, uuid, `track_get`, `tracking_render_url`);
+
+ await joinInterestGroup(
+ test, uuid,
+ {ads: [{renderURL: trackingRenderURL}]});
+
+ let abortController = new AbortController();
+ let fencedFrameConfig = await runBasicFledgeTestExpectingWinner(
+ test, uuid, {signal: abortController.signal});
+
+ // Aborting now should have no effect - in particular, it should still be
+ // possible to navigate to the winning ad, and it should still send reports.
+ abortController.abort('reason');
+
+ // Load the fencedFrameConfig in a fenced frame, and make sure reports are
+ // still sent and the render URL still loaded.
+ createAndNavigateFencedFrame(test, fencedFrameConfig);
+ await waitForObservedRequests(
+ uuid,
+ [trackingRenderURL, createBidderReportURL(uuid), createSellerReportURL(uuid)]);
+}, 'Abort signalled after auction completes.');
+
+promise_test(async test => {
+ const uuid = generateUuid(test);
+
+ await joinInterestGroup(
+ test, uuid,
+ { biddingLogicURL: createBiddingScriptURL(
+ { allowComponentAuction: true })});
+
+
+ let abortController = new AbortController();
+ let componentAuctionConfig = {
+ seller: window.location.origin,
+ decisionLogicURL: createDecisionScriptURL(uuid),
+ interestGroupBuyers: [window.location.origin],
+ signal: abortController.signal
+ };
+
+ let auctionConfigOverrides = {
+ decisionLogicURL: createDecisionScriptURL(uuid),
+ interestGroupBuyers: [],
+ componentAuctions: [componentAuctionConfig]
+ };
+
+ abortController.abort();
+ // Aborting a component auction has no effect.
+ await runBasicFledgeTestExpectingWinner(test, uuid, auctionConfigOverrides);
+}, 'Abort component auction.');
diff --git a/testing/web-platform/tests/fledge/tentative/auction-config-passed-to-worklets.https.window.js b/testing/web-platform/tests/fledge/tentative/auction-config-passed-to-worklets.https.window.js
new file mode 100644
index 0000000000..c78a27bb87
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/auction-config-passed-to-worklets.https.window.js
@@ -0,0 +1,208 @@
+// META: script=/resources/testdriver.js
+// META: script=/common/utils.js
+// META: script=resources/fledge-util.sub.js
+// META: script=/common/subset-tests.js
+// META: timeout=long
+// META: variant=?1-5
+// META: variant=?6-10
+// META: variant=?11-15
+// META: variant=?16-last
+
+"use strict;"
+
+// These tests focus on making sure AuctionConfig fields are passed to seller worklets,
+// and are normalized if necessary. This test does not check the behaviors of the
+// fields.
+
+const makeTest = ({
+ // Test name.
+ name,
+ // AuctionConfig field name.
+ fieldName,
+ // AuctionConfig field value, both expected in worklets and acution in the
+ // auction. If undefined, value will not be set in auctionConfig, and will
+ // be expected to also not be set in the auctionConfig passed to worklets.
+ fieldValue,
+ // Additional values to use in the AuctionConfig passed to runAdAuction().
+ // If it contains a value for the key specified in `fieldName`, that takes
+ // precedent over `fieldValue`.
+ auctionConfigOverrides = {}
+}) => {
+ subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ if (!(fieldName in auctionConfigOverrides) && fieldValue !== undefined)
+ auctionConfigOverrides[fieldName] = fieldValue;
+
+ let comparison = `deepEquals(auctionConfig["${fieldName}"], ${JSON.stringify(fieldValue)})`;
+ // In the case it's undefined, require value not to be set.
+ if (fieldValue === undefined)
+ comparison = `!("${fieldName}" in auctionConfig)`;
+
+ // Prefer to use `auctionConfigOverrides.seller` if present. Treat it as a URL
+ // and then convert it to an origin because one test passes in a URL.
+ let origin = location.origin;
+ if (auctionConfigOverrides.seller)
+ origin = new URL(auctionConfigOverrides.seller).origin;
+
+ auctionConfigOverrides.decisionLogicURL = createDecisionScriptURL(
+ uuid,
+ { origin: origin,
+ scoreAd:
+ `if (!${comparison})
+ throw "Unexpected value: " + JSON.stringify(auctionConfig["${fieldName}"]);`,
+ reportResult:
+ `let error = '';
+ if (!${comparison})
+ error += "_unexpected_value:" + JSON.stringify(auctionConfig["${fieldName}"]);
+ sendReportTo("${createSellerReportURL(uuid)}" + error);` }),
+
+ // Join an interest group so the auction has a winner. The details of the
+ // interest group do not matter.
+ await joinInterestGroup(test, uuid);
+ await runBasicFledgeAuctionAndNavigate(test, uuid, auctionConfigOverrides);
+ await waitForObservedRequests(
+ uuid, [createBidderReportURL(uuid), createSellerReportURL(uuid)]);
+ }, name);
+};
+
+makeTest({
+ name: 'AuctionConfig.seller.',
+ fieldName: 'seller',
+ fieldValue: OTHER_ORIGIN1
+});
+
+makeTest({
+ name: 'AuctionConfig.seller with non-normalized origin.',
+ fieldName: 'seller',
+ fieldValue: OTHER_ORIGIN1,
+ auctionConfigOverrides: {seller: ` ${OTHER_ORIGIN1.toUpperCase()} `}
+});
+
+makeTest({
+ name: 'AuctionConfig.seller is URL.',
+ fieldName: 'seller',
+ fieldValue: OTHER_ORIGIN1,
+ auctionConfigOverrides: {seller: OTHER_ORIGIN1 + "/Foopy"}
+});
+
+makeTest({
+ name: 'AuctionConfig.trustedScoringSignalsURL passed to seller worklets.',
+ fieldName: 'trustedScoringSignalsURL',
+ fieldValue: `${OTHER_ORIGIN1}${BASE_PATH}this-file-does-not-exist.json`,
+ auctionConfigOverrides: {seller: OTHER_ORIGIN1}
+});
+
+makeTest({
+ name: 'AuctionConfig.trustedScoringSignalsURL with non-normalized values.',
+ fieldName: 'trustedScoringSignalsURL',
+ fieldValue: `${OTHER_ORIGIN1}${BASE_PATH}this-file-does-not-exist.json`,
+ auctionConfigOverrides: {
+ seller: OTHER_ORIGIN1,
+ trustedScoringSignalsURL:
+ `${OTHER_ORIGIN1.toUpperCase()}${BASE_PATH}this-file-does-not-exist.json`
+ }
+});
+
+makeTest({
+ name: 'AuctionConfig.trustedScoringSignalsKeys not set.',
+ fieldName: 'trustedScoringSignalsKeys',
+ fieldValue: undefined
+});
+
+makeTest({
+ name: 'AuctionConfig.interestGroupBuyers.',
+ fieldName: 'interestGroupBuyers',
+ fieldValue: [OTHER_ORIGIN1, location.origin, OTHER_ORIGIN2]
+});
+
+makeTest({
+ name: 'AuctionConfig.interestGroupBuyers with non-normalized values.',
+ fieldName: 'interestGroupBuyers',
+ fieldValue: [OTHER_ORIGIN1, location.origin, OTHER_ORIGIN2],
+ auctionConfigOverrides: {
+ interestGroupBuyers: [
+ ` ${OTHER_ORIGIN1} `,
+ location.origin.toUpperCase(),
+ `${OTHER_ORIGIN2}/Foo`]
+ }
+});
+
+makeTest({
+ name: 'AuctionConfig.nonStandardField.',
+ fieldName: 'nonStandardField',
+ fieldValue: undefined,
+ aucitonConfigOverrides: {nonStandardField: 'This value should not be passed to worklets'}
+});
+
+makeTest({
+ name: 'AuctionConfig.requestedSize not set.',
+ fieldName: 'requestedSize',
+ fieldValue: undefined
+});
+
+makeTest({
+ name: 'AuctionConfig.requestedSize in pixels.',
+ fieldName: 'requestedSize',
+ fieldValue: {width: '100px', height: '200px'}
+});
+
+makeTest({
+ name: 'AuctionConfig.requestedSize in implicit pixels.',
+ fieldName: 'requestedSize',
+ fieldValue: {width: '100px', height: '200px'},
+ auctionConfigOverrides: {fieldValue: {width: '100', height: '200'}}
+});
+
+makeTest({
+ name: 'AuctionConfig.requestedSize in screen units.',
+ fieldName: 'requestedSize',
+ fieldValue: {width: '70sw', height: '80sh'}
+});
+
+makeTest({
+ name: 'AuctionConfig.requestedSize in inverse screen units.',
+ fieldName: 'requestedSize',
+ fieldValue: {width: '70sh', height: '80sw'}
+});
+
+makeTest({
+ name: 'AuctionConfig.requestedSize in mixed units.',
+ fieldName: 'requestedSize',
+ fieldValue: {width: '100px', height: '80sh'}
+});
+
+makeTest({
+ name: 'AuctionConfig.requestedSize with decimals.',
+ fieldName: 'requestedSize',
+ fieldValue: {width: '70.5sw', height: '80.56sh'}
+});
+
+makeTest({
+ name: 'AuctionConfig.requestedSize with non-normalized values.',
+ fieldName: 'requestedSize',
+ fieldValue: {width: '100px', height: '200.5px'},
+ auctionConfigOverrides: {fieldValue: {width: ' 100.0px', height: '200.50px'}}
+});
+
+makeTest({
+ name: 'Unset AuctionConfig.allSlotsRequestedSizes.',
+ fieldName: 'allSlotsRequestedSizes',
+ fieldValue: undefined
+});
+
+makeTest({
+ name: 'AuctionConfig.allSlotsRequestedSizes.',
+ fieldName: 'allSlotsRequestedSizes',
+ fieldValue: [{width: '100px', height: '200px'}, {width: '70sh', height: '80sw'}]
+});
+
+makeTest({
+ name: 'AuctionConfig.allSlotsRequestedSizes with non-normalized values.',
+ fieldName: 'allSlotsRequestedSizes',
+ fieldValue: [{width: '100px', height: '200.5px'},
+ {width: '70sh', height: '80.5sw'}],
+ auctionConfigOverrides: {fieldValue:
+ [{width: ' 100', height: '200.50px '},
+ {width: ' 70.00sh ', height: '80.50sw'}]}
+});
diff --git a/testing/web-platform/tests/fledge/tentative/auction-config.https.window.js b/testing/web-platform/tests/fledge/tentative/auction-config.https.window.js
new file mode 100644
index 0000000000..3b5814b5d4
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/auction-config.https.window.js
@@ -0,0 +1,392 @@
+// META: script=/resources/testdriver.js
+// META: script=/common/utils.js
+// META: script=resources/fledge-util.sub.js
+// META: script=/common/subset-tests.js
+// META: timeout=long
+// META: variant=?1-5
+// META: variant=?6-10
+// META: variant=?11-15
+// META: variant=?16-20
+// META: variant=?21-25
+// META: variant=?26-30
+// META: variant=?31-35
+// META: variant=?36-last
+
+"use strict;"
+
+// The tests in this file focus on calls to runAdAuction with various
+// auctionConfigs.
+
+// We handle promise rejections ourselves.
+setup({ allow_uncaught_exception: true });
+
+// Helper for when we expect it to happen.
+const interceptUnhandledRejection = () => {
+ let invokePromiseResolved;
+ let eventHandler = event => {
+ event.preventDefault();
+ invokePromiseResolved(event.reason);
+ }
+ window.addEventListener("unhandledrejection", eventHandler, {once: true});
+ return new Promise((resolved) => {
+ invokePromiseResolved = resolved;
+ });
+}
+
+// Helper for when we expect it to not happen. This relies on the event
+// dispatching being sync.
+const unexpectedUnhandledRejection = () => {
+ let o = { sawError : false }
+ window.addEventListener("unhandledrejection", event => {
+ o.sawError = true;
+ }, {once: true});
+ return o;
+}
+
+const makeTest = ({
+ // Test name
+ name,
+ // Expectation function (EXPECT_NULL, etc.)
+ expect,
+ // Overrides to the auction config.
+ auctionConfigOverrides = {},
+ // Expectation for a promise error.
+ expectPromiseError,
+}) => {
+ subsetTest(promise_test, async test => {
+ let waitPromiseError, dontExpectPromiseError;
+ if (expectPromiseError) {
+ waitPromiseError = interceptUnhandledRejection();
+ } else {
+ dontExpectPromiseError = unexpectedUnhandledRejection();
+ }
+
+ const uuid = generateUuid(test);
+ // Join an interest group so the auction actually runs.
+ await joinInterestGroup(test, uuid);
+ let auctionResult;
+ try {
+ auctionResult = await runBasicFledgeAuction(test, uuid, auctionConfigOverrides);
+ } catch (e) {
+ auctionResult = e;
+ }
+ expect(auctionResult);
+
+ if (expectPromiseError) {
+ expectPromiseError(await waitPromiseError);
+ } else {
+ assert_false(dontExpectPromiseError.sawError,
+ "Should not see a promise error");
+ }
+ }, name);
+};
+
+// Expect an unsuccessful auction (yielding null).
+const EXPECT_NO_WINNER = auctionResult => {
+ assert_equals(auctionResult, null, 'Auction unexpected had a winner');
+};
+
+// Expect an exception of the given type.
+const EXPECT_EXCEPTION = exceptionType => auctionResult => {
+ assert_not_equals(auctionResult, null, "got null instead of expected error");
+ assert_true(auctionResult instanceof Error, "did not get expected error: " + auctionResult);
+ assert_throws_js(exceptionType, () => { throw auctionResult; });
+};
+
+const EXPECT_PROMISE_ERROR = auctionResult => {
+ assert_not_equals(auctionResult, null, "got null instead of expected error");
+ assert_true(auctionResult instanceof TypeError,
+ "did not get expected error type: " + auctionResult);
+}
+
+makeTest({
+ name: 'no buyers => no winners',
+ expect: EXPECT_NO_WINNER,
+ auctionConfigOverrides: {interestGroupBuyers: []},
+});
+
+makeTest({
+ name: 'seller is not an https URL',
+ expect: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: {seller: "ftp://not-https"},
+});
+
+makeTest({
+ name: 'decisionLogicURL is invalid',
+ expect: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: { decisionLogicURL: "https://foo:99999999999" },
+});
+
+makeTest({
+ name: 'decisionLogicURL is cross-origin with seller',
+ expect: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: { decisionLogicURL: "https://example.com" },
+});
+
+makeTest({
+ name: 'trustedScoringSignalsURL is invalid',
+ expect: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: { trustedScoringSignalsURL: "https://foo:99999999999" },
+});
+
+makeTest({
+ name: 'trustedScoringSignalsURL is cross-origin with seller',
+ expect: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: { trustedScoringSignalsURL: "https://example.com" },
+});
+
+makeTest({
+ name: 'interestGroupBuyer is invalid',
+ expect: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: { interestGroupBuyers: ["https://foo:99999999999"] },
+});
+
+makeTest({
+ name: 'interestGroupBuyer is not https',
+ expect: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: { interestGroupBuyers: ["http://example.com"] },
+});
+
+makeTest({
+ name: 'only one interestGroupBuyer is invalid',
+ expect: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: {
+ interestGroupBuyers: ["https://example.com", "https://foo:99999999999"],
+ },
+});
+
+makeTest({
+ name: 'only one interestGroupBuyer is not https',
+ expect: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: {
+ interestGroupBuyers: ["https://example.com", "http://example.com"],
+ },
+});
+
+makeTest({
+ name: 'auctionSignals is invalid as JSON',
+ expect: EXPECT_PROMISE_ERROR,
+ expectPromiseError: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: { auctionSignals: { sig: BigInt(13) } },
+});
+
+makeTest({
+ name: 'sellerSignals is invalid as JSON',
+ expect: EXPECT_PROMISE_ERROR,
+ expectPromiseError: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: { sellerSignals: { sig: BigInt(13) } },
+});
+
+makeTest({
+ name: 'directFromSellerSignals is invalid',
+ expect: EXPECT_PROMISE_ERROR,
+ expectPromiseError: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: { directFromSellerSignals: "https://foo:99999999999" },
+});
+
+makeTest({
+ name: 'directFromSellerSignals is cross-origin with seller',
+ expect: EXPECT_PROMISE_ERROR,
+ expectPromiseError: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: { directFromSellerSignals: "https://example.com" },
+});
+
+makeTest({
+ name: 'directFromSellerSignals has nonempty query',
+ expect: EXPECT_PROMISE_ERROR,
+ expectPromiseError: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: { directFromSellerSignals: window.location.origin + "?foo=bar" },
+});
+
+makeTest({
+ name: 'perBuyerSignals has invalid URL in a key',
+ expect: EXPECT_PROMISE_ERROR,
+ expectPromiseError: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: { perBuyerSignals: { "https://foo:99999999999" : {} }},
+});
+
+makeTest({
+ name: 'perBuyerSignals value is invalid as JSON',
+ expect: EXPECT_PROMISE_ERROR,
+ expectPromiseError: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: {
+ perBuyerSignals: { "https://example.com" : { sig: BigInt(1) },
+ }},
+});
+
+makeTest({
+ name: 'perBuyerGroupLimits has invalid URL in a key',
+ expect: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: { perBuyerGroupLimits: { "https://foo:99999999999" : 5 }},
+});
+
+makeTest({
+ name: 'perBuyerExperimentGroupIds has invalid URL in a key',
+ expect: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: { perBuyerExperimentGroupIds: { "https://foo:99999999999" : 11 }},
+});
+
+makeTest({
+ name: 'perBuyerPrioritySignals has invalid URL in a key',
+ expect: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: {
+ perBuyerPrioritySignals: { "https://foo:99999999999" : { sig: 2.5} },
+ },
+});
+
+makeTest({
+ name: 'perBuyerPrioritySignals has a value with a key with prefix "browserSignals"',
+ expect: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: {
+ perBuyerPrioritySignals: { "https://example.com" : { "browserSignals.foo" : true } },
+ },
+});
+
+makeTest({
+ name: 'component auctions are not allowed within component auctions',
+ expect: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: {
+ interestGroupBuyers: undefined,
+ componentAuctions: [
+ {
+ seller: window.location.origin,
+ decisionLogicURL: window.location.origin,
+ interestGroupBuyers: undefined,
+ componentAuctions: [
+ {
+ seller: window.location.origin,
+ decisionLogicURL: window.location.origin,
+ }
+ ],
+ },
+ ],
+ },
+});
+
+makeTest({
+ name: 'component auctions are not allowed with interestGroupBuyers',
+ expect: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: {
+ interestGroupBuyers: ["https://example.com"],
+ componentAuctions: [
+ {
+ seller: window.location.origin,
+ decisionLogicURL: window.location.origin,
+ interestGroupBuyers: [],
+ },
+ ],
+ },
+});
+
+makeTest({
+ name: 'perBuyerCurrencies with invalid currency',
+ expect: EXPECT_PROMISE_ERROR,
+ expectPromiseError: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: {perBuyerCurrencies: {'*': 'Dollars'}}
+});
+
+makeTest({
+ name: 'perBuyerCurrencies with invalid currency map key',
+ expect: EXPECT_PROMISE_ERROR,
+ expectPromiseError: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: {perBuyerCurrencies: {'example': 'USD'}}
+});
+
+makeTest({
+ name: 'perBuyerCurrencies with non-https currency map key',
+ expect: EXPECT_PROMISE_ERROR,
+ expectPromiseError: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: {perBuyerCurrencies: {'http://example.org/': 'USD'}}
+});
+
+makeTest({
+ name: 'perBuyerCurrencies not convertible to dictionary',
+ expect: EXPECT_PROMISE_ERROR,
+ expectPromiseError: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: {perBuyerCurrencies: 123}
+});
+
+makeTest({
+ name: 'requestedSize has no width',
+ expect: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: {requestedSize: {height: '100'}}
+});
+
+makeTest({
+ name: 'requestedSize has no height',
+ expect: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: {requestedSize: {width: '100'}}
+});
+
+makeTest({
+ name: 'requestedSize width not a number',
+ expect: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: {requestedSize: {width: '10 0', height: '100'}}
+});
+
+makeTest({
+ name: 'requestedSize height not a number',
+ expect: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: {requestedSize: {width: '100', height: '10 0'}}
+});
+
+makeTest({
+ name: 'requestedSize 0',
+ expect: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: {requestedSize: {width: '0', height: '100'}}
+});
+
+makeTest({
+ name: 'requestedSize space before units',
+ expect: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: {requestedSize: {width: '100 px', height: '100'}}
+});
+
+makeTest({
+ name: 'requestedSize leading 0',
+ expect: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: {requestedSize: {width: '0100', height: '100'}}
+});
+
+makeTest({
+ name: 'requestedSize invalid unit type',
+ expect: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: {requestedSize: {width: '100furlongs', height: '100'}}
+});
+
+makeTest({
+ name: 'requestedSize hexideximal',
+ expect: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: {requestedSize: {width: '0x100', height: '100'}}
+});
+
+makeTest({
+ name: 'Empty allSlotsRequestedSizes',
+ expect: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: {allSlotsRequestedSizes: []}
+});
+
+makeTest({
+ name: 'allSlotsRequestedSizes without matching value in requestedSize',
+ expect: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: {requestedSize:
+ {width: '100', height: '100'},
+ allSlotsRequestedSizes:
+ [{width: '100', height: '101'}]}
+});
+
+makeTest({
+ name: 'allSlotsRequestedSizes has duplicate values',
+ expect: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: {allSlotsRequestedSizes:
+ [{width: '100', height: '100'},
+ {width: '100', height: '100'}]}
+});
+
+makeTest({
+ name: 'allSlotsRequestedSizes has invalid value',
+ expect: EXPECT_EXCEPTION(TypeError),
+ auctionConfigOverrides: {allSlotsRequestedSizes:
+ [{width: '100', height: '100'},
+ {width: '200furlongs', height: '200'}]}
+});
diff --git a/testing/web-platform/tests/fledge/tentative/clear-origin-joined-ad-interest-groups.https.window.js b/testing/web-platform/tests/fledge/tentative/clear-origin-joined-ad-interest-groups.https.window.js
new file mode 100644
index 0000000000..7d6e715ac4
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/clear-origin-joined-ad-interest-groups.https.window.js
@@ -0,0 +1,312 @@
+// META: script=/resources/testdriver.js
+// META: script=/common/utils.js
+// META: script=resources/fledge-util.sub.js
+// META: script=/common/subset-tests.js
+// META: timeout=long
+// META: variant=?1-4
+// META: variant=?5-8
+// META: variant=?9-12
+// META: variant=?13-last
+
+"use strict;"
+
+///////////////////////////////////////////////////////////////////////////////
+// Basic tests with no interest groups joined.
+///////////////////////////////////////////////////////////////////////////////
+
+subsetTest(promise_test, async test => {
+ await navigator.clearOriginJoinedAdInterestGroups(window.location.origin);
+}, 'clearOriginJoinedAdInterestGroups(), no groups joined, no group list.');
+
+subsetTest(promise_test, async test => {
+ await navigator.clearOriginJoinedAdInterestGroups(window.location.origin, []);
+}, 'clearOriginJoinedAdInterestGroups(), no groups joined, group list.');
+
+subsetTest(promise_test, async test => {
+ try {
+ await navigator.clearOriginJoinedAdInterestGroups(OTHER_ORIGIN1);
+ throw 'Exception unexpectedly not thrown';
+ } catch (e) {
+ if (!(e instanceof DOMException) || e.name !== 'NotAllowedError') {
+ throw 'Wrong exception thrown: ' + e.toString();
+ }
+ }
+}, 'clearOriginJoinedAdInterestGroups(), cross-origin, no groups joined, no group list.');
+
+subsetTest(promise_test, async test => {
+ try {
+ await navigator.clearOriginJoinedAdInterestGroups(OTHER_ORIGIN1, []);
+ throw 'Exception unexpectedly not thrown';
+ } catch (e) {
+ if (!(e instanceof DOMException) || e.name !== 'NotAllowedError') {
+ throw 'Wrong exception thrown: ' + e.toString();
+ }
+ }
+}, 'clearOriginJoinedAdInterestGroups(), cross-origin, no groups joined, group list.');
+
+///////////////////////////////////////////////////////////////////////////////
+// Tests where interest groups are all owned by document.location.origin.
+///////////////////////////////////////////////////////////////////////////////
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ // Join 3 groups.
+ await joinInterestGroup(test, uuid);
+ await joinInterestGroup(test, uuid, {name: 'group 2'});
+ await joinInterestGroup(test, uuid, {name: 'group 3'});
+
+ // A single clear should leave them all.
+ await navigator.clearOriginJoinedAdInterestGroups(window.location.origin);
+
+ // Confirm that they were left.
+ await runBasicFledgeTestExpectingNoWinner(test, uuid);
+}, 'clearOriginJoinedAdInterestGroups(), multiple groups joined, no group list.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ let group1ReportURL = createBidderReportURL(uuid, /*id=*/'1');
+ let group2ReportURL = createBidderReportURL(uuid, /*id=*/'2');
+ let group3ReportURL = createBidderReportURL(uuid, /*id=*/'3');
+
+ // Join 3 groups, with distinct report URLs and increasing bid amounts.
+ // Set "executionMode" to "group-by-origin" for two of them, since cross-origin
+ // leaves removes all groups joined from the other origin with that execution
+ // mode. Since clearOriginJoinedAdInterestGroups() only leaves interest
+ // groups joined on the current origin, the executionMode should not matter.
+ await joinInterestGroup(
+ test, uuid,
+ { name: 'group 1',
+ executionMode: 'group-by-origin',
+ biddingLogicURL: createBiddingScriptURL(
+ { bid: 1, reportWin: `sendReportTo("${group1ReportURL}");`})});
+ await joinInterestGroup(
+ test, uuid,
+ { name: 'group 2',
+ biddingLogicURL: createBiddingScriptURL(
+ { bid: 2, reportWin: `sendReportTo("${group2ReportURL}");`})});
+ await joinInterestGroup(
+ test, uuid,
+ { name: 'group 3',
+ executionMode: 'group-by-origin',
+ biddingLogicURL: createBiddingScriptURL(
+ { bid: 3, reportWin: `sendReportTo("${group3ReportURL}");`})});
+
+ // Group 3 should win an auction, since it bids the most.
+ await runBasicFledgeAuctionAndNavigate(test, uuid);
+ await waitForObservedRequests(
+ uuid, [group3ReportURL, createSellerReportURL(uuid)]);
+ await fetch(createCleanupURL(uuid));
+
+ // Clear, leaving group 1 in place, and run an auction, which group 1 should win.
+ await navigator.clearOriginJoinedAdInterestGroups(
+ window.location.origin, ['group 1']);
+ await runBasicFledgeAuctionAndNavigate(test, uuid);
+ await waitForObservedRequests(
+ uuid, [group1ReportURL, createSellerReportURL(uuid)]);
+
+ // Clear with an empty list, which should leave group 1 as well. Verify it can't
+ // win an auction.
+ await navigator.clearOriginJoinedAdInterestGroups(window.location.origin, []);
+ await runBasicFledgeTestExpectingNoWinner(test, uuid);
+}, 'clearOriginJoinedAdInterestGroups(), multiple groups joined, group list.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ // Join an interest group in a same-origin top-level window.
+ await joinInterestGroupInTopLevelWindow(test, uuid, window.location.origin);
+
+ // Make sure it was joined.
+ await runBasicFledgeTestExpectingWinner(test, uuid);
+
+ // Call "clearOriginJoinedAdInterestGroups()", which should leave the interest
+ // group, since it was joined from a same-origin main frame.
+ await navigator.clearOriginJoinedAdInterestGroups(window.location.origin);
+
+ // Make sure group was left.
+ await runBasicFledgeTestExpectingNoWinner(test, uuid);
+}, 'clearOriginJoinedAdInterestGroups(), group joined from same-origin top-level context.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ // Create top-level browsing context for another origin, and have it join an
+ // interest group owned by this document's origin.
+ let topLevelWindow = await createTopLevelWindow(test, OTHER_ORIGIN1);
+ let interestGroup = JSON.stringify(
+ createInterestGroupForOrigin(uuid, window.location.origin));
+ await runInFrame(test, topLevelWindow,
+ `await joinCrossOriginInterestGroup(test_instance, "${uuid}",
+ "${window.location.origin}",
+ ${interestGroup});`);
+
+ // Call "clearOriginJoinedAdInterestGroups()", which should not leave the interest
+ // group, since it was joined from a cross-origin main frame.
+ await navigator.clearOriginJoinedAdInterestGroups(window.location.origin);
+
+ // Make sure group was not left.
+ await runBasicFledgeTestExpectingWinner(test, uuid);
+}, 'clearOriginJoinedAdInterestGroups(), group joined from cross-origin top-level context.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ await joinInterestGroup(test, uuid);
+
+ // In a cross-origin iframe, call clearOriginJoinedAdInterestGroups() both for the
+ // iframe's origin and for the main frame's origin. The latter should throw an
+ // exception, and neither should manage to leave the interest group.
+ let iframe = await createIframe(test, OTHER_ORIGIN1, 'join-ad-interest-group');
+ await runInFrame(test, iframe,
+ `// Call clearOriginJoinedAdInterestGroups() with the iframe's origin.
+ await navigator.clearOriginJoinedAdInterestGroups(window.location.origin);
+ try {
+ // Call clearOriginJoinedAdInterestGroups() with the main frame's origin.
+ await navigator.clearOriginJoinedAdInterestGroups("${window.location.origin}");
+ } catch (e) {
+ assert_true(e instanceof DOMException, "DOMException thrown");
+ assert_equals(e.name, "NotAllowedError", "NotAllowedError DOMException thrown");
+ return {result: "success"};
+ }
+ throw "Exception unexpectedly not thrown";`);
+
+ // Confirm that the interest group was not left.
+ await runBasicFledgeTestExpectingWinner(test, uuid);
+}, "clearOriginJoinedAdInterestGroups(), cross-origin iframe tries to leave parent frame's group.");
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ // The possible results of calling clearOriginJoinedAdInterestGroups():
+
+ // Doesn't throw an exception.
+ const noExpectionURL = createTrackerURL(origin, uuid, "track_get", "no_exception");
+ // Throws the exception it's expected to.
+ const exceptionURL = createTrackerURL(origin, uuid, "track_get", "exception");
+ // Throws the wrong exception.
+ const badExpectionURL = createTrackerURL(origin, uuid, "track_get", "bad_exception");
+
+ // Create a render URL that calls clearOriginJoinedAdInterestGroups() and
+ // then requests one of the above tracking URLs, based on the resulting
+ // behaviot.
+ const renderURL = createRenderURL(
+ uuid,
+ `async function TryClear() {
+ try {
+ await navigator.clearOriginJoinedAdInterestGroups(
+ "${window.location.origin}");
+ await fetch("${noExpectionURL}");
+ } catch (e) {
+ if (e instanceof DOMException && e.name === "NotAllowedError") {
+ await fetch("${exceptionURL}");
+ } else {
+ await fetch("${badExpectionURL}");
+ }
+ }
+ }
+
+ TryClear();`);
+
+ await joinInterestGroup(
+ test, uuid,
+ {ads: [{ renderURL: renderURL}]});
+
+ await runBasicFledgeAuctionAndNavigate(test, uuid);
+
+ // This should wait until the clear call has thrown an exception.
+ await waitForObservedRequests(
+ uuid,
+ [createBidderReportURL(uuid), createSellerReportURL(uuid), exceptionURL]);
+
+ // Check the interest group was not left.
+ await runBasicFledgeTestExpectingWinner(test, uuid);
+}, 'clearOriginJoinedAdInterestGroups() in ad fenced frame throws an exception.');
+
+///////////////////////////////////////////////////////////////////////////////
+// Tests where some interest groups are owned by another origin.
+///////////////////////////////////////////////////////////////////////////////
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ // Join interest group in iframe and make sure it was joined.
+ let iframe = await createIframe(test, OTHER_ORIGIN1, 'join-ad-interest-group');
+ await runInFrame(test, iframe,
+ `await joinInterestGroup(test_instance, "${uuid}");
+ await runBasicFledgeTestExpectingWinner(test_instance, "${uuid}");`);
+
+ // In the main frame, Call clearOriginJoinedAdInterestGroups() for both the main
+ // frame's origin, and the origin of the iframe / joined interest group. Neither
+ // should leave the group, and the second should throw.
+ await navigator.clearOriginJoinedAdInterestGroups(window.location.origin);
+ try {
+ await navigator.clearOriginJoinedAdInterestGroups(OTHER_ORIGIN1);
+ throw 'Exception unexpectedly not thrown';
+ } catch (e) {
+ if (!(e instanceof DOMException) || e.name !== 'NotAllowedError') {
+ throw 'Wrong exception thrown: ' + e.toString();
+ }
+ }
+
+ // In an iframe, confirm the group was never left.
+ await runInFrame(test, iframe,
+ `await runBasicFledgeTestExpectingWinner(test_instance, "${uuid}");`);
+}, 'clearOriginJoinedAdInterestGroups(). Cross-origin interest group joined in iframe, try to clear in main frame.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ let iframe = await createIframe(test, OTHER_ORIGIN1, 'join-ad-interest-group');
+ await runInFrame(test, iframe,
+ `await joinInterestGroup(test_instance, "${uuid}");
+
+ // Confirm that trying to clear the interest group using the main frame's
+ // origin throws, and does not leave the group.
+ try {
+ await navigator.clearOriginJoinedAdInterestGroups("${window.location.origin}");
+ throw 'Exception unexpectedly not thrown';
+ } catch (e) {
+ if (!(e instanceof DOMException) || e.name !== 'NotAllowedError') {
+ throw 'Wrong exception thrown: ' + e.toString();
+ }
+ }
+ await runBasicFledgeTestExpectingWinner(test_instance, "${uuid}");`);
+}, 'clearOriginJoinedAdInterestGroups(). Cross-origin interest group joined in iframe, clear call in iframe passing main frame origin.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ let iframe = await createIframe(test, OTHER_ORIGIN1, 'join-ad-interest-group');
+ await runInFrame(test, iframe,
+ `await joinInterestGroup(test_instance, "${uuid}");
+
+ // Clear call with the origin of the cross-origin iframe.
+ // This should successfully leave the interest group.
+ await navigator.clearOriginJoinedAdInterestGroups("${OTHER_ORIGIN1}");
+
+ // Verify the group was left.
+ await runBasicFledgeTestExpectingNoWinner(test_instance, "${uuid}");`);
+}, 'clearOriginJoinedAdInterestGroups(). Cross-origin interest group joined in iframe, clear call in iframe passing iframe origin.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ // Join an OTHER_ORIGIN1 interest group in an OTHER_ORIGIN1 main frame.
+ let topLevelWindow = await createTopLevelWindow(test, OTHER_ORIGIN1);
+ await runInFrame(test, topLevelWindow,
+ `await joinInterestGroup(test_instance, "${uuid}");`);
+
+ let iframe = await createIframe(test, OTHER_ORIGIN1, 'join-ad-interest-group');
+
+ await runInFrame(test, iframe,
+ `// Clear call from an OTHER_ORIGIN1 iframe on a different
+ // origin's main frame. This should not clear the interest
+ // group that was just joined, because the joining origin
+ // does not match.
+ await navigator.clearOriginJoinedAdInterestGroups("${OTHER_ORIGIN1}");
+
+ // Verify the group was not left.
+ await runBasicFledgeTestExpectingWinner(test_instance, "${uuid}");`);
+}, 'clearOriginJoinedAdInterestGroups(). Cross-origin interest group joined from another joining origin, clear call in iframe.');
diff --git a/testing/web-platform/tests/fledge/tentative/component-ads.https.window.js b/testing/web-platform/tests/fledge/tentative/component-ads.https.window.js
new file mode 100644
index 0000000000..7e98570b9e
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/component-ads.https.window.js
@@ -0,0 +1,449 @@
+// META: script=/resources/testdriver.js
+// META: script=/common/utils.js
+// META: script=/common/subset-tests.js
+// META: script=resources/fledge-util.sub.js
+// META: timeout=long
+// META: variant=?1-5
+// META: variant=?6-10
+// META: variant=?11-15
+// META: variant=?16-last
+
+"use strict";
+
+// Creates a tracker URL for a component ad. These are fetched from component ad URLs.
+function createComponentAdTrackerURL(uuid, id) {
+ return createTrackerURL(window.location.origin, uuid, 'track_get',
+ `component_ad_${id}`)
+}
+
+// Returns a component ad render URL that fetches the corresponding component ad
+// tracker URL.
+function createComponentAdRenderURL(uuid, id) {
+ return createRenderURL(
+ uuid,
+ `fetch("${createComponentAdTrackerURL(uuid, id)}");`);
+}
+
+// Runs a generic component ad loading test. It joins an interest group with a
+// "numComponentAdsInInterestGroup" component ads. The IG will make a bid that
+// potentially includes some of them. Then an auction will be run, component
+// ads potentially will be loaded in nested fenced frame within the main frame,
+// and the test will make sure that each component ad render URL that should have
+// been loaded in an iframe was indeed loaded.
+//
+// Joins an interest group that has "numComponentAdsInInterestGroup" component ads.
+//
+// "componentAdsInBid" is a list of 0-based indices of which of those ads will be
+// included in the bid. It may contain duplicate component ads. If it's null then the
+// bid will have no adComponents field, while if it is empty, the bid will have an empty
+// adComponents field.
+//
+// "componentAdsToLoad" is another list of 0-based ad components, but it's the index of
+// fenced frame configs in the top frame ad's getNestedConfigs(). It may also contain
+// duplicates to load a particular ad twice.
+//
+// If "adMetadata" is true, metadata is added to each component ad. Only integer metadata
+// is used, relying on renderURL tests to cover other types of renderURL metadata.
+async function runComponentAdLoadingTest(test, uuid, numComponentAdsInInterestGroup,
+ componentAdsInBid, componentAdsToLoad,
+ adMetadata = false) {
+ let interestGroupAdComponents = [];
+ for (let i = 0; i < numComponentAdsInInterestGroup; ++i) {
+ const componentRenderURL = createComponentAdRenderURL(uuid, i);
+ let adComponent = {renderURL: componentRenderURL};
+ if (adMetadata)
+ adComponent.metadata = i;
+ interestGroupAdComponents.push(adComponent);
+ }
+
+ const renderURL = createRenderURL(
+ uuid,
+ `// "status" is passed to the beacon URL, to be verified by waitForObservedRequests().
+ let status = "ok";
+ const componentAds = window.fence.getNestedConfigs()
+ if (componentAds.length != 40)
+ status = "unexpected getNestedConfigs() length";
+ for (let i of ${JSON.stringify(componentAdsToLoad)}) {
+ let fencedFrame = document.createElement("fencedframe");
+ fencedFrame.mode = "opaque-ads";
+ fencedFrame.config = componentAds[i];
+ document.body.appendChild(fencedFrame);
+ }
+
+ window.fence.reportEvent({eventType: "beacon",
+ eventData: status,
+ destination: ["buyer"]});`);
+
+ let bid = {bid:1, render: renderURL};
+ if (componentAdsInBid) {
+ bid.adComponents = [];
+ for (let index of componentAdsInBid) {
+ bid.adComponents.push(interestGroupAdComponents[index].renderURL);
+ }
+ }
+
+ // In these tests, the bidder should always request a beacon URL.
+ let expectedTrackerURLs = [`${createBidderBeaconURL(uuid)}, body: ok`];
+ // Figure out which, if any, elements of "componentAdsToLoad" correspond to
+ // component ads listed in bid.adComponents, and for those ads, add a tracker URL
+ // to "expectedTrackerURLs".
+ if (componentAdsToLoad && bid.adComponents) {
+ for (let index of componentAdsToLoad) {
+ if (index < componentAdsInBid.length)
+ expectedTrackerURLs.push(createComponentAdTrackerURL(uuid, componentAdsInBid[index]));
+ }
+ }
+
+ await joinInterestGroup(
+ test, uuid,
+ { biddingLogicURL:
+ createBiddingScriptURL({
+ generateBid:
+ `let expectedAdComponents = ${JSON.stringify(interestGroupAdComponents)};
+ let adComponents = interestGroup.adComponents;
+ if (adComponents.length !== expectedAdComponents.length)
+ throw "Unexpected adComponents";
+ for (let i = 0; i < adComponents.length; ++i) {
+ if (adComponents[i].renderURL !== expectedAdComponents[i].renderURL ||
+ adComponents[i].metadata !== expectedAdComponents[i].metadata) {
+ throw "Unexpected adComponents";
+ }
+ }
+ return ${JSON.stringify(bid)}`,
+ reportWin:
+ `registerAdBeacon({beacon: '${createBidderBeaconURL(uuid)}'});` }),
+ ads: [{renderURL: renderURL}],
+ adComponents: interestGroupAdComponents});
+
+ if (!bid.adComponents || bid.adComponents.length === 0) {
+ await runBasicFledgeAuctionAndNavigate(
+ test, uuid,
+ {decisionLogicURL: createDecisionScriptURL(
+ uuid,
+ { scoreAd: `if (browserSignals.adComponents !== undefined)
+ throw "adComponents should be undefined"`})});
+ } else {
+ await runBasicFledgeAuctionAndNavigate(
+ test, uuid,
+ {decisionLogicURL: createDecisionScriptURL(
+ uuid,
+ { scoreAd:
+ `if (JSON.stringify(browserSignals.adComponents) !==
+ '${JSON.stringify(bid.adComponents)}') {
+ throw "Unexpected adComponents: " + JSON.stringify(browserSignals.adComponents);
+ }`})});
+ }
+
+ await waitForObservedRequests(uuid, expectedTrackerURLs);
+}
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ const renderURL = createRenderURL(uuid, `let status = "ok";
+ const nestedConfigsLength = window.fence.getNestedConfigs().length
+ // "getNestedConfigs()" should return a list of 40 configs, to avoid leaking
+ // whether there were any component URLs to the page.
+ if (nestedConfigsLength != 40)
+ status = "unexpected getNestedConfigs() length: " + nestedConfigsLength;
+ window.fence.reportEvent({eventType: "beacon",
+ eventData: status,
+ destination: ["buyer"]});`);
+ await joinInterestGroup(
+ test, uuid,
+ { biddingLogicURL:
+ createBiddingScriptURL({
+ generateBid:
+ 'if (interestGroup.componentAds !== undefined) throw "unexpected componentAds"',
+ reportWin:
+ `registerAdBeacon({beacon: "${createBidderBeaconURL(uuid)}"});` }),
+ ads: [{renderUrl: renderURL}]});
+ await runBasicFledgeAuctionAndNavigate(
+ test, uuid,
+ {decisionLogicURL: createDecisionScriptURL(
+ uuid,
+ { scoreAd: `if (browserSignals.adComponents !== undefined)
+ throw "adComponents should be undefined"`})});
+ await waitForObservedRequests(uuid, [`${createBidderBeaconURL(uuid)}, body: ok`]);
+}, 'Group has no component ads, no adComponents in bid.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ await joinGroupAndRunBasicFledgeTestExpectingNoWinner(
+ test,
+ {uuid: uuid,
+ interestGroupOverrides: {
+ biddingLogicURL:
+ createBiddingScriptURL({
+ generateBid:
+ `return {bid: 1,
+ render: interestGroup.ads[0].renderUrl,
+ adComponents: []};`})}});
+}, 'Group has no component ads, adComponents in bid is empty array.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await runComponentAdLoadingTest(
+ test, uuid, /*numComponentAdsInInterestGroup=*/2, /*componentAdsInBid=*/null,
+ // Try to load ad components, even though there are none. This should load
+ // about:blank in those frames, though that's not testible.
+ // The waitForObservedRequests() call may see extra requests, racily, if
+ // component ads not found in the bid are used.
+ /*componentAdsToLoad=*/[0, 1]);
+}, 'Group has component ads, but not used in bid (no adComponents field).');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await runComponentAdLoadingTest(
+ test, uuid, /*numComponentAdsInInterestGroup=*/2, /*componentAdsInBid=*/[],
+ // Try to load ad components, even though there are none. This should load
+ // about:blank in those frames, though that's not testible.
+ // The waitForObservedRequests() call may see extra requests, racily, if
+ // component ads not found in the bid are used.
+ /*componentAdsToLoad=*/[0, 1]);
+}, 'Group has component ads, but not used in bid (adComponents field empty array).');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await runComponentAdLoadingTest(
+ test, uuid, /*numComponentAdsInInterestGroup=*/2, /*componentAdsInBid=*/null,
+ // Try to load ad components, even though there are none. This should load
+ // about:blank in those frames, though that's not testible.
+ // The waitForObservedRequests() call may see extra requests, racily, if
+ // component ads not found in the bid are used.
+ /*componentAdsToLoad=*/[0, 1], /*adMetadata=*/true);
+}, 'Unused component ads with metadata.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ await joinGroupAndRunBasicFledgeTestExpectingNoWinner(
+ test,
+ { uuid: uuid,
+ interestGroupOverrides: {
+ biddingLogicURL:
+ createBiddingScriptURL({
+ generateBid:
+ `return {bid: 1,
+ render: interestGroup.ads[0].renderUrl,
+ adComponents: ["https://random.url.test/"]};`}),
+ adComponents: [{renderURL: createComponentAdRenderURL(uuid, 0)}]}});
+}, 'Unknown component ad URL in bid.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ await joinGroupAndRunBasicFledgeTestExpectingNoWinner(
+ test,
+ { uuid: uuid,
+ interestGroupOverrides: {
+ biddingLogicURL:
+ createBiddingScriptURL({
+ generateBid:
+ `return {bid: 1,
+ render: interestGroup.ads[0].renderUrl,
+ adComponents: [interestGroup.ads[0].renderUrl]};`}),
+ adComponents: [{renderURL: createComponentAdRenderURL(uuid, 0)}]}});
+}, 'Render URL used as component ad URL in bid.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ await joinGroupAndRunBasicFledgeTestExpectingNoWinner(
+ test,
+ { uuid: uuid,
+ interestGroupOverrides: {
+ biddingLogicURL:
+ createBiddingScriptURL({
+ generateBid:
+ `return {bid: 1, render: interestGroup.adComponents[0].renderURL};`}),
+ adComponents: [{renderURL: createComponentAdRenderURL(uuid, 0)}]}});
+}, 'Component ad URL used as render URL.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await runComponentAdLoadingTest(test, uuid, /*numComponentAdsInInterestGroup=*/2,
+ /*componentAdsInBid=*/[0, 1], /*componentAdsToLoad=*/[0, 1]);
+}, '2 of 2 component ads in bid and then shown.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await runComponentAdLoadingTest(test, uuid, /*numComponentAdsInInterestGroup=*/2,
+ /*componentAdsInBid=*/[0, 1], /*componentAdsToLoad=*/[0, 1],
+ /*adMetadata=*/true);
+}, '2 of 2 component ads in bid and then shown, with metadata.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await runComponentAdLoadingTest(test, uuid, /*numComponentAdsInInterestGroup=*/20,
+ /*componentAdsInBid=*/[3, 10], /*componentAdsToLoad=*/[0, 1]);
+}, '2 of 20 component ads in bid and then shown.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ const intsUpTo19 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19];
+ await runComponentAdLoadingTest(test, uuid, /*numComponentAdsInInterestGroup=*/20,
+ /*componentAdsInBid=*/intsUpTo19,
+ /*componentAdsToLoad=*/intsUpTo19);
+}, '20 of 20 component ads in bid and then shown.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ const intsUpTo39 = [];
+ for (let i = 0; i < 40; ++i) {
+ intsUpTo39.push(i);
+ }
+ await runComponentAdLoadingTest(
+ test, uuid, /*numComponentAdsInInterestGroup=*/ 40,
+ /*componentAdsInBid=*/ intsUpTo39,
+ /*componentAdsToLoad=*/ intsUpTo39);
+}, '40 of 40 component ads in bid and then shown.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await runComponentAdLoadingTest(test, uuid, /*numComponentAdsInInterestGroup=*/20,
+ /*componentAdsInBid=*/[1, 2, 3, 4, 5, 6],
+ /*componentAdsToLoad=*/[1, 3]);
+}, '6 of 20 component ads in bid, 2 shown.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ // It should be possible to load ads multiple times. Each loaded ad should request a new tracking
+ // URLs, as they're fetched via XHRs, rather than reporting.
+ await runComponentAdLoadingTest(test, uuid, /*numComponentAdsInInterestGroup=*/4,
+ /*componentAdsInBid=*/[0, 1, 2, 3],
+ /*componentAdsToLoad=*/[0, 1, 1, 0, 3, 3, 2, 2, 1, 0]);
+}, '4 of 4 component ads shown multiple times.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await runComponentAdLoadingTest(test, uuid, /*numComponentAdsInInterestGroup=*/2,
+ /*componentAdsInBid=*/[0, 0, 0, 0],
+ /*componentAdsToLoad=*/[0, 1, 2, 3]);
+}, 'Same component ad used multiple times in bid.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ // The bid only has one component ad, but the renderURL tries to load 5 component ads.
+ // The others should all be about:blank. Can't test that, so just make sure there aren't
+ // more requests than expected, and there's no crash.
+ await runComponentAdLoadingTest(test, uuid, /*numComponentAdsInInterestGroup=*/2,
+ /*componentAdsInBid=*/[0],
+ /*componentAdsToLoad=*/[4, 3, 2, 1, 0]);
+}, 'Load component ads not in bid.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ const renderURL = createRenderURL(uuid);
+
+ let adComponents = [];
+ let adComponentsList = [];
+ for (let i = 0; i < 41; ++i) {
+ let componentRenderURL = createComponentAdTrackerURL(uuid, i);
+ adComponents.push({renderURL: componentRenderURL});
+ adComponentsList.push(componentRenderURL);
+ }
+
+ await joinGroupAndRunBasicFledgeTestExpectingNoWinner(
+ test,
+ { uuid: uuid,
+ interestGroupOverrides: {
+ biddingLogicURL:
+ createBiddingScriptURL({
+ generateBid:
+ `return {bid: 1,
+ render: "${renderURL}",
+ adComponents: ${JSON.stringify(adComponentsList)}};`}),
+ ads: [{renderURL: renderURL}],
+ adComponents: adComponents}});
+}, '41 component ads not allowed in bid.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ const renderURL = createRenderURL(uuid);
+
+ let adComponents = [];
+ let adComponentsList = [];
+ for (let i = 0; i < 41; ++i) {
+ let componentRenderURL = createComponentAdTrackerURL(uuid, i);
+ adComponents.push({renderURL: componentRenderURL});
+ adComponentsList.push(adComponents[0].renderURL);
+ }
+
+ await joinGroupAndRunBasicFledgeTestExpectingNoWinner(
+ test,
+ { uuid: uuid,
+ interestGroupOverrides: {
+ biddingLogicURL:
+ createBiddingScriptURL({
+ generateBid:
+ `return {bid: 1,
+ render: "${renderURL}",
+ adComponents: ${JSON.stringify(adComponentsList)}};`}),
+ ads: [{renderURL: renderURL}],
+ adComponents: adComponents}});
+}, 'Same component ad not allowed 41 times in bid.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ // The component ad's render URL will try to send buyer and seller reports,
+ // which should not be sent (but not throw an exception), and then request a
+ // a tracker URL via fetch, which should be requested from the server.
+ const componentRenderURL =
+ createRenderURL(
+ uuid,
+ `window.fence.reportEvent({eventType: "beacon",
+ eventData: "Should not be sent",
+ destination: ["buyer", "seller"]});
+ fetch("${createComponentAdTrackerURL(uuid, 0)}");`);
+
+ const renderURL = createRenderURL(
+ uuid,
+ `let fencedFrame = document.createElement("fencedframe");
+ fencedFrame.mode = "opaque-ads";
+ fencedFrame.config = window.fence.getNestedConfigs()[0];
+ document.body.appendChild(fencedFrame);
+
+ async function waitForRequestAndSendBeacons() {
+ // Wait for the nested fenced frame to request its tracker URL.
+ await waitForObservedRequests("${uuid}", ["${createComponentAdTrackerURL(uuid, 0)}"]);
+
+ // Now that the tracker URL has been received, the component ad has tried to
+ // send a beacon, so have the main renderURL send a beacon, which should succeed
+ // and should hopefully be sent after the component ad's beacon, if it was
+ // going to (incorrectly) send one.
+ window.fence.reportEvent({eventType: "beacon",
+ eventData: "top-ad",
+ destination: ["buyer", "seller"]});
+ }
+ waitForRequestAndSendBeacons();`);
+
+ await joinInterestGroup(
+ test, uuid,
+ { biddingLogicURL:
+ createBiddingScriptURL({
+ generateBid:
+ `return {bid: 1,
+ render: "${renderURL}",
+ adComponents: ["${componentRenderURL}"]};`,
+ reportWin:
+ `registerAdBeacon({beacon: '${createBidderBeaconURL(uuid)}'});` }),
+ ads: [{renderURL: renderURL}],
+ adComponents: [{renderURL: componentRenderURL}]});
+
+ await runBasicFledgeAuctionAndNavigate(
+ test, uuid,
+ {decisionLogicURL: createDecisionScriptURL(
+ uuid,
+ { reportResult: `registerAdBeacon({beacon: '${createSellerBeaconURL(uuid)}'});` }) });
+
+ // Only the renderURL should have sent any beacons, though the component ad should have sent
+ // a tracker URL fetch request.
+ await waitForObservedRequests(uuid, [createComponentAdTrackerURL(uuid, 0),
+ `${createBidderBeaconURL(uuid)}, body: top-ad`,
+ `${createSellerBeaconURL(uuid)}, body: top-ad`]);
+
+
+}, 'Reports not sent from component ad.');
diff --git a/testing/web-platform/tests/fledge/tentative/component-auction.https.window.js b/testing/web-platform/tests/fledge/tentative/component-auction.https.window.js
new file mode 100644
index 0000000000..63771d42b8
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/component-auction.https.window.js
@@ -0,0 +1,719 @@
+// META: script=/resources/testdriver.js
+// META: script=/common/utils.js
+// META: script=/common/subset-tests.js
+// META: script=resources/fledge-util.sub.js
+// META: timeout=long
+// META: variant=?1-5
+// META: variant=?6-10
+// META: variant=?11-15
+// META: variant=?16-last
+
+"use strict";
+
+// Creates an AuctionConfig with a single component auction.
+function createComponentAuctionConfig(uuid) {
+ let componentAuctionConfig = {
+ seller: window.location.origin,
+ decisionLogicURL: createDecisionScriptURL(uuid),
+ interestGroupBuyers: [window.location.origin]
+ };
+
+ return {
+ seller: window.location.origin,
+ decisionLogicURL: createDecisionScriptURL(uuid),
+ interestGroupBuyers: [],
+ componentAuctions: [componentAuctionConfig]
+ };
+}
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ await joinInterestGroup(
+ test, uuid,
+ { biddingLogicURL: createBiddingScriptURL()});
+
+ await runBasicFledgeTestExpectingNoWinner(test, uuid, createComponentAuctionConfig(uuid));
+}, 'Component auction allowed not specified by bidder.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ await joinInterestGroup(
+ test, uuid,
+ { biddingLogicURL: createBiddingScriptURL(
+ { allowComponentAuction: false })});
+
+ await runBasicFledgeTestExpectingNoWinner(test, uuid, createComponentAuctionConfig(uuid));
+}, 'Component auction not allowed by bidder.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ await joinInterestGroup(
+ test, uuid,
+ { biddingLogicURL: createBiddingScriptURL(
+ { allowComponentAuction: true })});
+
+ let auctionConfig = createComponentAuctionConfig(uuid);
+ auctionConfig.componentAuctions[0].decisionLogicURL = createDecisionScriptURL(
+ uuid,
+ { scoreAd: "return 5;" });
+
+ await runBasicFledgeTestExpectingNoWinner(test, uuid, auctionConfig);
+}, 'Component auction allowed not specified by component seller.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ await joinInterestGroup(
+ test, uuid,
+ { biddingLogicURL: createBiddingScriptURL(
+ { allowComponentAuction: true })});
+
+ let auctionConfig = createComponentAuctionConfig(uuid);
+ auctionConfig.componentAuctions[0].decisionLogicURL = createDecisionScriptURL(
+ uuid,
+ { scoreAd: "return {desirability: 5, allowComponentAuction: false};" });
+
+ await runBasicFledgeTestExpectingNoWinner(test, uuid, auctionConfig);
+}, 'Component auction not allowed by component seller.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ await joinInterestGroup(
+ test, uuid,
+ { biddingLogicURL: createBiddingScriptURL(
+ { allowComponentAuction: true })});
+
+ let auctionConfig = createComponentAuctionConfig(uuid);
+ auctionConfig.decisionLogicURL = createDecisionScriptURL(
+ uuid,
+ { scoreAd: "return 5;" });
+
+ await runBasicFledgeTestExpectingNoWinner(test, uuid, auctionConfig);
+}, 'Component auction allowed not specified by top-level seller.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ await joinInterestGroup(
+ test, uuid,
+ { biddingLogicURL: createBiddingScriptURL(
+ { allowComponentAuction: true })});
+
+ let auctionConfig = createComponentAuctionConfig(uuid);
+ auctionConfig.interestGroupBuyers = [window.location.origin];
+
+ try {
+ await runBasicFledgeAuction(test, uuid, auctionConfig);
+ } catch (exception) {
+ assert_true(exception instanceof TypeError, "did not get expected error: " + exception);
+ return;
+ }
+ throw 'Exception unexpectedly not thrown.'
+}, 'Component auction top-level auction cannot have buyers.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ await joinInterestGroup(
+ test, uuid,
+ { biddingLogicURL: createBiddingScriptURL(
+ { allowComponentAuction: true })});
+
+ let auctionConfig = createComponentAuctionConfig(uuid);
+ auctionConfig.decisionLogicURL = createDecisionScriptURL(
+ uuid,
+ { scoreAd: "return {desirability: 5, allowComponentAuction: false};" });
+
+ await runBasicFledgeTestExpectingNoWinner(test, uuid, auctionConfig);
+}, 'Component auction not allowed by top-level seller.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ // Use distinct origins so can validate all origin parameters passed to worklets.
+ let bidder = OTHER_ORIGIN1;
+ let componentSeller = OTHER_ORIGIN2;
+ let topLevelSeller = OTHER_ORIGIN3;
+
+ let bidderReportURL = createBidderReportURL(uuid);
+ let componentSellerReportURL = createSellerReportURL(uuid, /*id=*/"component");
+ let topLevelSellerReportURL = createSellerReportURL(uuid, /*id=*/"top");
+
+ // Note that generateBid() and reportWin() receive slightly different
+ // "browserSignals" fields - only reportWin() gets "interestGroupOwner", so
+ // need different sets of checks for them.
+ await joinCrossOriginInterestGroup(
+ test, uuid, bidder,
+ { biddingLogicURL: createBiddingScriptURL(
+ { origin: bidder,
+ allowComponentAuction: true,
+ generateBid:
+ `if (browserSignals.seller !== "${componentSeller}")
+ throw "Unexpected seller: " + browserSignals.seller;
+ if (browserSignals.componentSeller !== undefined)
+ throw "Unexpected componentSeller: " + browserSignals.componentSeller;
+ if (browserSignals.topLevelSeller !== "${topLevelSeller}")
+ throw "Unexpected topLevelSeller: " + browserSignals.topLevelSeller;
+ if (browserSignals.interestGroupOwner !== undefined)
+ throw "Unexpected interestGroupOwner: " + browserSignals.interestGroupOwner;
+ if (browserSignals.topWindowHostname !== "${window.location.hostname}")
+ throw "Unexpected topWindowHostname: " + browserSignals.topWindowHostname;`,
+ reportWin:
+ `if (browserSignals.seller !== "${componentSeller}")
+ throw "Unexpected seller: " + browserSignals.seller;
+ if (browserSignals.componentSeller !== undefined)
+ throw "Unexpected componentSeller: " + browserSignals.componentSeller;
+ if (browserSignals.topLevelSeller !== "${topLevelSeller}")
+ throw "Unexpected topLevelSeller: " + browserSignals.topLevelSeller;
+ if (browserSignals.interestGroupOwner !== "${bidder}")
+ throw "Unexpected interestGroupOwner: " + browserSignals.interestGroupOwner;
+ if (browserSignals.topWindowHostname !== "${window.location.hostname}")
+ throw "Unexpected topWindowHostname: " + browserSignals.topWindowHostname;
+ sendReportTo("${bidderReportURL}");`})});
+
+ // Checks for scoreAd() and reportResult() for the component seller.
+ let componentSellerChecks =
+ `if (browserSignals.seller !== undefined)
+ throw "Unexpected seller: " + browserSignals.seller;
+ if (browserSignals.componentSeller !== undefined)
+ throw "Unexpected componentSeller: " + browserSignals.componentSeller;
+ if (browserSignals.topLevelSeller !== "${topLevelSeller}")
+ throw "Unexpected topLevelSeller: " + browserSignals.topLevelSeller;
+ if (browserSignals.interestGroupOwner !== "${bidder}")
+ throw "Unexpected interestGroupOwner: " + browserSignals.interestGroupOwner;
+ if (browserSignals.topWindowHostname !== "${window.location.hostname}")
+ throw "Unexpected topWindowHostname: " + browserSignals.topWindowHostname;`;
+
+ let componentAuctionConfig = {
+ seller: componentSeller,
+ decisionLogicURL: createDecisionScriptURL(
+ uuid,
+ { origin: componentSeller,
+ scoreAd: componentSellerChecks,
+ reportResult: `${componentSellerChecks}
+ sendReportTo("${componentSellerReportURL}");` }),
+ interestGroupBuyers: [bidder]
+ };
+
+ // Checks for scoreAd() and reportResult() for the top-level seller.
+ let topLevelSellerChecks =
+ `if (browserSignals.seller !== undefined)
+ throw "Unexpected seller: " + browserSignals.seller;
+ if (browserSignals.componentSeller !== "${componentSeller}")
+ throw "Unexpected componentSeller: " + browserSignals.componentSeller;
+ if (browserSignals.topLevelSeller !== undefined)
+ throw "Unexpected topLevelSeller: " + browserSignals.topLevelSeller;
+ if (browserSignals.interestGroupOwner !== "${bidder}")
+ throw "Unexpected interestGroupOwner: " + browserSignals.interestGroupOwner;
+ if (browserSignals.topWindowHostname !== "${window.location.hostname}")
+ throw "Unexpected topWindowHostname: " + browserSignals.topWindowHostname;`;
+
+ let auctionConfigOverrides = {
+ seller: topLevelSeller,
+ decisionLogicURL: createDecisionScriptURL(
+ uuid,
+ { origin: topLevelSeller,
+ scoreAd: topLevelSellerChecks,
+ reportResult: `${topLevelSellerChecks}
+ sendReportTo("${topLevelSellerReportURL}");` }),
+ interestGroupBuyers: [],
+ componentAuctions: [componentAuctionConfig]
+ };
+
+ await runBasicFledgeAuctionAndNavigate(test, uuid, auctionConfigOverrides);
+ await waitForObservedRequests(
+ uuid,
+ [bidderReportURL, componentSellerReportURL, topLevelSellerReportURL]);
+}, 'Component auction browserSignals origins.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ let bidderReportURL = createBidderReportURL(uuid);
+ let componentSellerReportURL = createSellerReportURL(uuid, /*id=*/"component");
+ let topLevelSellerReportURL = createSellerReportURL(uuid, /*id=*/"top");
+
+ await joinInterestGroup(
+ test, uuid,
+ { biddingLogicURL: createBiddingScriptURL(
+ { allowComponentAuction: true,
+ bid: 5,
+ reportWin:
+ `if (browserSignals.bid !== 5)
+ throw "Unexpected bid: " + browserSignals.bid;
+ sendReportTo("${bidderReportURL}");`})});
+
+ let auctionConfig = createComponentAuctionConfig(uuid);
+
+ auctionConfig.componentAuctions[0].decisionLogicURL =
+ createDecisionScriptURL(
+ uuid,
+ { scoreAd:
+ `if (bid !== 5)
+ throw "Unexpected component bid: " + bid`,
+ reportResult:
+ `if (browserSignals.bid !== 5)
+ throw "Unexpected component bid: " + browserSignals.bid;
+ if (browserSignals.modifiedBid !== undefined)
+ throw "Unexpected component modifiedBid: " + browserSignals.modifiedBid;
+ sendReportTo("${componentSellerReportURL}");` });
+
+ auctionConfig.decisionLogicURL =
+ createDecisionScriptURL(
+ uuid,
+ { scoreAd:
+ `if (bid !== 5)
+ throw "Unexpected top-level bid: " + bid`,
+ reportResult:
+ `if (browserSignals.bid !== 5)
+ throw "Unexpected top-level bid: " + browserSignals.bid;
+ if (browserSignals.modifiedBid !== undefined)
+ throw "Unexpected top-level modifiedBid: " + browserSignals.modifiedBid;
+ sendReportTo("${topLevelSellerReportURL}");` });
+
+ await runBasicFledgeAuctionAndNavigate(test, uuid, auctionConfig);
+ await waitForObservedRequests(
+ uuid,
+ [bidderReportURL, componentSellerReportURL, topLevelSellerReportURL]);
+}, 'Component auction unmodified bid.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ let bidderReportURL = createBidderReportURL(uuid);
+ let componentSellerReportURL = createSellerReportURL(uuid, /*id=*/"component");
+ let topLevelSellerReportURL = createSellerReportURL(uuid, /*id=*/"top");
+
+ await joinInterestGroup(
+ test, uuid,
+ { biddingLogicURL: createBiddingScriptURL(
+ { allowComponentAuction: true,
+ bid: 5,
+ reportWin:
+ `if (browserSignals.bid !== 5)
+ throw "Unexpected bid: " + browserSignals.bid;
+ sendReportTo("${bidderReportURL}");`})});
+
+ let auctionConfig = createComponentAuctionConfig(uuid);
+
+ auctionConfig.componentAuctions[0].decisionLogicURL =
+ createDecisionScriptURL(
+ uuid,
+ { scoreAd:
+ `if (bid !== 5)
+ throw "Unexpected component bid: " + bid
+ return {desirability: 5, allowComponentAuction: true, bid: 4};`,
+ reportResult:
+ `if (browserSignals.bid !== 5)
+ throw "Unexpected component bid: " + browserSignals.bid;
+ if (browserSignals.modifiedBid !== 4)
+ throw "Unexpected component modifiedBid: " + browserSignals.modifiedBid;
+ sendReportTo("${componentSellerReportURL}");` });
+
+ auctionConfig.decisionLogicURL =
+ createDecisionScriptURL(
+ uuid,
+ { scoreAd:
+ `if (bid !== 4)
+ throw "Unexpected top-level bid: " + bid`,
+ reportResult:
+ `if (browserSignals.bid !== 4)
+ throw "Unexpected top-level bid: " + browserSignals.bid;
+ if (browserSignals.modifiedBid !== undefined)
+ throw "Unexpected top-level modifiedBid: " + browserSignals.modifiedBid;
+ sendReportTo("${topLevelSellerReportURL}");` });
+
+ await runBasicFledgeAuctionAndNavigate(test, uuid, auctionConfig);
+ await waitForObservedRequests(
+ uuid,
+ [bidderReportURL, componentSellerReportURL, topLevelSellerReportURL]);
+}, 'Component auction modified bid.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ let bidderReportURL = createBidderReportURL(uuid);
+ let componentSellerReportURL = createSellerReportURL(uuid, /*id=*/"component");
+ let topLevelSellerReportURL = createSellerReportURL(uuid, /*id=*/"top");
+
+ await joinInterestGroup(
+ test, uuid,
+ { biddingLogicURL: createBiddingScriptURL(
+ { allowComponentAuction: true,
+ bid: 5,
+ reportWin:
+ `if (browserSignals.bid !== 5)
+ throw "Unexpected bid: " + browserSignals.bid;
+ sendReportTo("${bidderReportURL}");`})});
+
+ let auctionConfig = createComponentAuctionConfig(uuid);
+
+ auctionConfig.componentAuctions[0].decisionLogicURL =
+ createDecisionScriptURL(
+ uuid,
+ { scoreAd:
+ `if (bid !== 5)
+ throw "Unexpected component bid: " + bid
+ return {desirability: 5, allowComponentAuction: true, bid: 5};`,
+ reportResult:
+ `if (browserSignals.bid !== 5)
+ throw "Unexpected component bid: " + browserSignals.bid;
+ if (browserSignals.modifiedBid !== 5)
+ throw "Unexpected component modifiedBid: " + browserSignals.modifiedBid;
+ sendReportTo("${componentSellerReportURL}");` });
+
+ auctionConfig.decisionLogicURL =
+ createDecisionScriptURL(
+ uuid,
+ { scoreAd:
+ `if (bid !== 5)
+ throw "Unexpected top-level bid: " + bid`,
+ reportResult:
+ `if (browserSignals.bid !== 5)
+ throw "Unexpected top-level bid: " + browserSignals.bid;
+ if (browserSignals.modifiedBid !== undefined)
+ throw "Unexpected top-level modifiedBid: " + browserSignals.modifiedBid;
+ sendReportTo("${topLevelSellerReportURL}");` });
+
+ await runBasicFledgeAuctionAndNavigate(test, uuid, auctionConfig);
+ await waitForObservedRequests(
+ uuid,
+ [bidderReportURL, componentSellerReportURL, topLevelSellerReportURL]);
+}, 'Component auction modified bid to same value.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ let bidderReportURL = createBidderReportURL(uuid);
+ let componentSellerReportURL = createSellerReportURL(uuid, /*id=*/"component");
+ let topLevelSellerReportURL = createSellerReportURL(uuid, /*id=*/"top");
+
+ await joinInterestGroup(
+ test, uuid,
+ { biddingLogicURL: createBiddingScriptURL(
+ { allowComponentAuction: true,
+ bid: 5,
+ reportWin:
+ `if (browserSignals.bid !== 5)
+ throw "Unexpected bid: " + browserSignals.bid;
+ sendReportTo("${bidderReportURL}");`})});
+
+ let auctionConfig = createComponentAuctionConfig(uuid);
+
+ auctionConfig.componentAuctions[0].decisionLogicURL =
+ createDecisionScriptURL(
+ uuid,
+ { scoreAd:
+ `if (bid !== 5)
+ throw "Unexpected component bid: " + bid`,
+ reportResult:
+ `if (browserSignals.bid !== 5)
+ throw "Unexpected component bid: " + browserSignals.bid;
+ if (browserSignals.modifiedBid !== undefined)
+ throw "Unexpected component modifiedBid: " + browserSignals.modifiedBid;
+ sendReportTo("${componentSellerReportURL}");` });
+
+ auctionConfig.decisionLogicURL =
+ createDecisionScriptURL(
+ uuid,
+ { scoreAd:
+ `if (bid !== 5)
+ throw "Unexpected top-level bid: " + bid
+ return {desirability: 5, allowComponentAuction: true, bid: 4};`,
+ reportResult:
+ `if (browserSignals.bid !== 5)
+ throw "Unexpected top-level bid: " + browserSignals.bid;
+ if (browserSignals.modifiedBid !== undefined)
+ throw "Unexpected top-level modifiedBid: " + browserSignals.modifiedBid;
+ sendReportTo("${topLevelSellerReportURL}");` });
+
+ await runBasicFledgeAuctionAndNavigate(test, uuid, auctionConfig);
+ await waitForObservedRequests(
+ uuid,
+ [bidderReportURL, componentSellerReportURL, topLevelSellerReportURL]);
+}, 'Top-level auction cannot modify bid.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ let bidderReportURL = createBidderReportURL(uuid);
+ let componentSellerReportURL = createSellerReportURL(uuid, /*id=*/"component");
+ let topLevelSellerReportURL = createSellerReportURL(uuid, /*id=*/"top");
+
+ await joinInterestGroup(
+ test, uuid,
+ { biddingLogicURL: createBiddingScriptURL(
+ { allowComponentAuction: true,
+ reportWin:
+ `if (browserSignals.desirability !== undefined)
+ throw "Unexpected desirability: " + browserSignals.desirability;
+ sendReportTo("${bidderReportURL}");`})});
+
+ let auctionConfig = createComponentAuctionConfig(uuid);
+
+ auctionConfig.componentAuctions[0].decisionLogicURL =
+ createDecisionScriptURL(
+ uuid,
+ { scoreAd:
+ `return {desirability: 3, allowComponentAuction: true};`,
+ reportResult:
+ `if (browserSignals.desirability !== 3)
+ throw "Unexpected component desirability: " + browserSignals.desirability;
+ sendReportTo("${componentSellerReportURL}");` });
+
+ auctionConfig.decisionLogicURL =
+ createDecisionScriptURL(
+ uuid,
+ { scoreAd:
+ `return {desirability: 4, allowComponentAuction: true};`,
+ reportResult:
+ `if (browserSignals.desirability !== 4)
+ throw "Unexpected component desirability: " + browserSignals.desirability;
+ sendReportTo("${topLevelSellerReportURL}");` });
+
+ await runBasicFledgeAuctionAndNavigate(test, uuid, auctionConfig);
+ await waitForObservedRequests(
+ uuid,
+ [bidderReportURL, componentSellerReportURL, topLevelSellerReportURL]);
+}, 'Component auction desirability.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ // An auction with two components, each of which has a distinct bidder origin,
+ // so the bidder in the second component is OTHER_ORIGIN1). The bidder in the
+ // first component auction bids more and is given the highest of all
+ // desirability scores in the auction by its component seller, but the
+ // top-level seller prefers bidder 2.
+ let bidder1ReportURL = createBidderReportURL(uuid, /*id=*/1);
+ let bidder2ReportURL = createBidderReportURL(uuid, /*id=*/2);
+ let componentSeller1ReportURL = createSellerReportURL(uuid, /*id=*/"component1");
+ let componentSeller2ReportURL = createSellerReportURL(uuid, /*id=*/"component2");
+ let topLevelSellerReportURL = createSellerReportURL(uuid, /*id=*/"top");
+
+ await Promise.all([
+ joinInterestGroup(
+ test, uuid,
+ { biddingLogicURL: createBiddingScriptURL(
+ { bid: 10,
+ allowComponentAuction: true,
+ reportWin:
+ `sendReportTo("${bidder1ReportURL}");`})}),
+ joinCrossOriginInterestGroup(test, uuid, OTHER_ORIGIN1,
+ { biddingLogicURL: createBiddingScriptURL(
+ { origin: OTHER_ORIGIN1,
+ bid: 2,
+ allowComponentAuction: true,
+ reportWin:
+ `if (browserSignals.bid !== 2)
+ throw "Unexpected bid: " + browserSignals.bid;
+ sendReportTo("${bidder2ReportURL}");`})})
+ ]);
+
+ let auctionConfig = createComponentAuctionConfig(uuid);
+
+ auctionConfig.componentAuctions[0].decisionLogicURL =
+ createDecisionScriptURL(
+ uuid,
+ { scoreAd:
+ `return {desirability: 10, allowComponentAuction: true};`,
+ reportResult:
+ `sendReportTo("${componentSeller1ReportURL}");` });
+
+ auctionConfig.componentAuctions[1] = {
+ ...auctionConfig.componentAuctions[0],
+ interestGroupBuyers: [OTHER_ORIGIN1],
+ decisionLogicURL: createDecisionScriptURL(
+ uuid,
+ { scoreAd:
+ `return {desirability: 1, allowComponentAuction: true};`,
+ reportResult:
+ `if (browserSignals.desirability !== 1)
+ throw "Unexpected component desirability: " + browserSignals.desirability;
+ sendReportTo("${componentSeller2ReportURL}");` })
+ }
+
+ auctionConfig.decisionLogicURL =
+ createDecisionScriptURL(
+ uuid,
+ { scoreAd:
+ `return {desirability: 11 - bid, allowComponentAuction: true};`,
+ reportResult:
+ `if (browserSignals.desirability !== 9)
+ throw "Unexpected component desirability: " + browserSignals.desirability;
+ sendReportTo("${topLevelSellerReportURL}");` });
+
+ await runBasicFledgeAuctionAndNavigate(test, uuid, auctionConfig);
+ await waitForObservedRequests(
+ uuid,
+ [bidder2ReportURL, componentSeller2ReportURL, topLevelSellerReportURL]);
+}, 'Component auction desirability two sellers, two bidders.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ let renderURL1 = createRenderURL(uuid);
+ let renderURL2 = createRenderURL(uuid, /*script=*/';');
+
+ // The same bidder uses different ads, bids, and reporting URLs for different
+ // component sellers.
+ let bidderReportURL1 = createBidderReportURL(uuid, /*id=*/1);
+ let bidderReportURL2 = createBidderReportURL(uuid, /*id=*/2);
+ let componentSeller1ReportURL = createSellerReportURL(uuid, /*id=*/"component1");
+ let componentSeller2ReportURL = createSellerReportURL(uuid, /*id=*/"component2");
+ let topLevelSellerReportURL = createSellerReportURL(uuid, /*id=*/"top");
+
+ await joinInterestGroup(
+ test, uuid,
+ { ads: [{ renderURL: renderURL1 }, { renderURL: renderURL2 }],
+ biddingLogicURL: createBiddingScriptURL(
+ { allowComponentAuction: true,
+ generateBid:
+ // "auctionSignals" contains the bid and the report URL, to
+ // make the same bidder behave differently in the two
+ // auctions.
+ 'return auctionSignals;',
+ reportWin:
+ `if (browserSignals.renderURL !== "${renderURL2}")
+ throw "Wrong winner: " + browserSignals.renderURL;
+ sendReportTo(auctionSignals.reportURL);`})});
+
+ let auctionConfig = createComponentAuctionConfig(uuid);
+
+ auctionConfig.componentAuctions[0].decisionLogicURL =
+ createDecisionScriptURL(
+ uuid,
+ { scoreAd:
+ `return {desirability: 10, allowComponentAuction: true};`,
+ reportResult:
+ `sendReportTo("${componentSeller1ReportURL}");` });
+ // "auctionSignals" contains the bid and the report URL, to
+ // make the same bidder behave differently in the two
+ // auctions.
+ auctionConfig.componentAuctions[0].auctionSignals = {
+ bid: 10,
+ allowComponentAuction: true,
+ render: renderURL1,
+ reportURL: bidderReportURL1
+ };
+
+ auctionConfig.componentAuctions[1] = {
+ ...auctionConfig.componentAuctions[0],
+ auctionSignals: {
+ bid: 2,
+ allowComponentAuction: true,
+ render: renderURL2,
+ reportURL: bidderReportURL2
+ },
+ decisionLogicURL: createDecisionScriptURL(
+ uuid,
+ { scoreAd:
+ `return {desirability: 1, allowComponentAuction: true};`,
+ reportResult:
+ `if (browserSignals.desirability !== 1)
+ throw "Unexpected component desirability: " + browserSignals.desirability;
+ if (browserSignals.renderURL !== "${renderURL2}")
+ throw "Wrong winner: " + browserSignals.renderURL;
+ sendReportTo("${componentSeller2ReportURL}");` })
+ }
+
+ auctionConfig.decisionLogicURL =
+ createDecisionScriptURL(
+ uuid,
+ { scoreAd:
+ `return {desirability: 11 - bid, allowComponentAuction: true};`,
+ reportResult:
+ `if (browserSignals.desirability !== 9)
+ throw "Unexpected component desirability: " + browserSignals.desirability;
+ if (browserSignals.renderURL !== "${renderURL2}")
+ throw "Wrong winner: " + browserSignals.renderURL;
+ sendReportTo("${topLevelSellerReportURL}");` });
+
+ await runBasicFledgeAuctionAndNavigate(test, uuid, auctionConfig);
+ await waitForObservedRequests(
+ uuid,
+ [bidderReportURL2, componentSeller2ReportURL, topLevelSellerReportURL]);
+}, 'Component auction desirability and renderURL two sellers, one bidder.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ // The renderURLs / report URLs for the first/second iterations of the auction.
+ let renderURL1 = createRenderURL(uuid);
+ let renderURL2 = createRenderURL(uuid, /*script=*/';');
+ let bidderReportURL1 = createBidderReportURL(uuid, /*id=*/1);
+ let bidderReportURL2 = createBidderReportURL(uuid, /*id=*/2);
+ let seller1ReportURL = createSellerReportURL(uuid, /*id=*/1);
+ let seller2ReportURL = createSellerReportURL(uuid, /*id=*/2);
+
+ await joinInterestGroup(
+ test, uuid,
+ { ads: [{ renderURL: renderURL1 }, { renderURL: renderURL2 }],
+ biddingLogicURL: createBiddingScriptURL(
+ { allowComponentAuction: true,
+ generateBid:
+ `// If this is the first recorded win, use "renderURL1"
+ if (browserSignals.bidCount === 0 &&
+ browserSignals.prevWinsMs.length === 0) {
+ return {bid: 2, allowComponentAuction: true, render: "${renderURL1}"};
+ }
+
+ // Otherwise, check that a single bid and win were reported, despite the
+ // bidder bidding twice in the first auction, once for each component
+ // auction.
+ if (browserSignals.bidCount === 1 &&
+ browserSignals.prevWinsMs.length === 1 &&
+ typeof browserSignals.prevWinsMs[0][0] === "number" &&
+ browserSignals.prevWinsMs[0][1].renderURL === "${renderURL1}") {
+ return {bid: 1, allowComponentAuction: true, render: "${renderURL2}"};
+ }
+ throw "Unexpected biddingSignals: " + JSON.stringify(browserSignals);`,
+ reportWin:
+ `if (browserSignals.renderURL === "${renderURL1}")
+ sendReportTo("${bidderReportURL1}");
+ if (browserSignals.renderURL === "${renderURL2}")
+ sendReportTo("${bidderReportURL2}");`})});
+
+ // Auction has two component auctions with different sellers but the same
+ // single bidder. The first component auction only accepts bids with
+ // "renderURL1", the second only accepts bids with "renderURL2".
+ let auctionConfig = createComponentAuctionConfig(uuid);
+ auctionConfig.componentAuctions[0].decisionLogicURL =
+ createDecisionScriptURL(
+ uuid,
+ { scoreAd: `if (browserSignals.renderURL != '${renderURL1}')
+ throw 'Wrong ad';`,
+ reportResult: `sendReportTo('${seller1ReportURL}');`}
+ );
+
+ auctionConfig.componentAuctions[1] = {
+ seller: OTHER_ORIGIN1,
+ interestGroupBuyers: [window.location.origin],
+ decisionLogicURL: createDecisionScriptURL(
+ uuid,
+ { origin: OTHER_ORIGIN1,
+ scoreAd: `if (browserSignals.renderURL != '${renderURL2}')
+ throw 'Wrong ad';`,
+ reportResult: `sendReportTo('${seller2ReportURL}');`}
+ )
+ };
+
+ // In the first auction, the bidder should use "renderURL1", which the first
+ // component auction allows. `prevWinsMs` and `numBids` should be updated.
+ await runBasicFledgeAuctionAndNavigate(test, uuid, auctionConfig);
+ await waitForObservedRequests(
+ uuid,
+ [bidderReportURL1, seller1ReportURL]);
+
+ // In the second auction, the bidder should use "renderURL2", which the second
+ // component auction allows. `prevWinsMs` and `numBids` should reflect the updated
+ // value.
+ await runBasicFledgeAuctionAndNavigate(test, uuid, auctionConfig);
+ await waitForObservedRequests(
+ uuid,
+ [bidderReportURL1, seller1ReportURL, bidderReportURL2, seller2ReportURL]);
+}, `Component auction prevWinsMs and numBids updating in one component seller's auction, read in another's.`);
diff --git a/testing/web-platform/tests/fledge/tentative/cross-origin.https.window.js b/testing/web-platform/tests/fledge/tentative/cross-origin.https.window.js
new file mode 100644
index 0000000000..788558e5cf
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/cross-origin.https.window.js
@@ -0,0 +1,452 @@
+// META: script=/resources/testdriver.js
+// META: script=/common/utils.js
+// META: script=/common/subset-tests.js
+// META: script=resources/fledge-util.sub.js
+// META: timeout=long
+// META: variant=?1-4
+// META: variant=?5-8
+// META: variant=?9-12
+// META: variant=?13-last
+
+"use strict;"
+
+////////////////////////////////////////////////////////////////////////////////
+// Join interest group in iframe tests.
+////////////////////////////////////////////////////////////////////////////////
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ let iframe = await createIframe(test, document.location.origin);
+
+ // Join a same-origin InterestGroup in a iframe navigated to its origin.
+ await runInFrame(test, iframe, `await joinInterestGroup(test_instance, "${uuid}");`);
+
+ // Run an auction using window.location.origin as a bidder. The IG should
+ // make a bid and win an auction.
+ await runBasicFledgeTestExpectingWinner(test, uuid);
+}, 'Join interest group in same-origin iframe, default permissions.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ let iframe = await createIframe(test, OTHER_ORIGIN1);
+
+ // Join a cross-origin InterestGroup in a iframe navigated to its origin.
+ await runInFrame(test, iframe, `await joinInterestGroup(test_instance, "${uuid}");`);
+
+ // Run an auction in this frame using the other origin as a bidder. The IG should
+ // make a bid and win an auction.
+ //
+ // TODO: Once the permission defaults to not being able to join InterestGroups in
+ // cross-origin iframes, this auction should have no winner.
+ await runBasicFledgeTestExpectingWinner(
+ test, uuid,
+ { interestGroupBuyers: [OTHER_ORIGIN1],
+ scoreAd: `if (browserSignals.interestGroupOwner !== "${OTHER_ORIGIN1}")
+ throw "Wrong owner: " + browserSignals.interestGroupOwner`
+ });
+}, 'Join interest group in cross-origin iframe, default permissions.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ let iframe = await createIframe(test, OTHER_ORIGIN1, 'join-ad-interest-group');
+
+ // Join a cross-origin InterestGroup in a iframe navigated to its origin.
+ await runInFrame(test, iframe, `await joinInterestGroup(test_instance, "${uuid}");`);
+
+ // Run an auction in this frame using the other origin as a bidder. The IG should
+ // make a bid and win an auction.
+ await runBasicFledgeTestExpectingWinner(
+ test, uuid,
+ { interestGroupBuyers: [OTHER_ORIGIN1],
+ scoreAd: `if (browserSignals.interestGroupOwner !== "${OTHER_ORIGIN1}")
+ throw "Wrong owner: " + browserSignals.interestGroupOwner`
+ });
+}, 'Join interest group in cross-origin iframe with join-ad-interest-group permission.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ let iframe = await createIframe(test, OTHER_ORIGIN1, "join-ad-interest-group 'none'");
+
+ // Try to join an InterestGroup in a cross-origin iframe whose permissions policy
+ // blocks joining interest groups. An exception should be thrown, and the interest
+ // group should not be joined.
+ await runInFrame(test, iframe,
+ `try {
+ await joinInterestGroup(test_instance, "${uuid}");
+ } catch (e) {
+ assert_true(e instanceof DOMException, "DOMException thrown");
+ assert_equals(e.name, "NotAllowedError", "NotAllowedError DOMException thrown");
+ return {result: "success"};
+ }
+ return "exception unexpectedly not thrown";`);
+
+ // Run an auction in this frame using the other origin as a bidder. Since the join
+ // should have failed, the auction should have no winner.
+ await runBasicFledgeTestExpectingNoWinner(
+ test, uuid,
+ { interestGroupBuyers: [OTHER_ORIGIN1] });
+}, 'Join interest group in cross-origin iframe with join-ad-interest-group permission denied.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ let iframe = await createIframe(test, OTHER_ORIGIN1, 'join-ad-interest-group');
+
+ // Try to join an IG with the parent's origin as an owner in a cross-origin iframe.
+ // This should require a .well-known fetch to the parents origin, which will not
+ // grant permission. The case where permission is granted is not yet testable.
+ let interestGroup = JSON.stringify(createInterestGroupForOrigin(uuid, window.location.origin));
+ await runInFrame(test, iframe,
+ `try {
+ await joinInterestGroup(test_instance, "${uuid}", ${interestGroup});
+ } catch (e) {
+ assert_true(e instanceof DOMException, "DOMException thrown");
+ assert_equals(e.name, "NotAllowedError", "NotAllowedError DOMException thrown");
+ return {result: "success"};
+ }
+ return "exception unexpectedly not thrown";`);
+
+ // Run an auction with this page's origin as a bidder. Since the join
+ // should have failed, the auction should have no winner.
+ await runBasicFledgeTestExpectingNoWinner(test, uuid);
+}, "Join interest group owned by parent's origin in cross-origin iframe.");
+
+////////////////////////////////////////////////////////////////////////////////
+// Run auction in iframe tests.
+////////////////////////////////////////////////////////////////////////////////
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await joinInterestGroup(test, uuid);
+
+ let iframe = await createIframe(test, document.location.origin);
+
+ // Join a same-origin InterestGroup in a iframe navigated to its origin.
+ await runInFrame(test, iframe, `await joinInterestGroup(test_instance, "${uuid}");`);
+
+ // Run auction in same-origin iframe. This should succeed, by default.
+ await runInFrame(
+ test, iframe,
+ `await runBasicFledgeTestExpectingWinner(test_instance, "${uuid}");`);
+}, 'Run auction in same-origin iframe, default permissions.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ // Join an interest group owned by the the main frame's origin.
+ await joinInterestGroup(test, uuid);
+
+ let iframe = await createIframe(test, OTHER_ORIGIN1);
+
+ // Run auction in cross-origin iframe. Currently, this is allowed by default.
+ await runInFrame(
+ test, iframe,
+ `await runBasicFledgeTestExpectingWinner(
+ test_instance, "${uuid}",
+ {interestGroupBuyers: ["${window.location.origin}"]});`);
+}, 'Run auction in cross-origin iframe, default permissions.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ // Join an interest group owned by the the main frame's origin.
+ await joinInterestGroup(test, uuid);
+
+ let iframe = await createIframe(test, OTHER_ORIGIN1, "run-ad-auction");
+
+ // Run auction in cross-origin iframe that should allow the auction to occur.
+ await runInFrame(
+ test, iframe,
+ `await runBasicFledgeTestExpectingWinner(
+ test_instance, "${uuid}",
+ {interestGroupBuyers: ["${window.location.origin}"]});`);
+}, 'Run auction in cross-origin iframe with run-ad-auction permission.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ // No need to join any interest groups in this case - running an auction
+ // should only throw an exception based on permissions policy, regardless
+ // of whether there are any interest groups can participate.
+
+ let iframe = await createIframe(test, OTHER_ORIGIN1, "run-ad-auction 'none'");
+
+ // Run auction in cross-origin iframe that should not allow the auction to occur.
+ await runInFrame(
+ test, iframe,
+ `try {
+ await runBasicFledgeAuction(test_instance, "${uuid}");
+ } catch (e) {
+ assert_true(e instanceof DOMException, "DOMException thrown");
+ assert_equals(e.name, "NotAllowedError", "NotAllowedError DOMException thrown");
+ return {result: "success"};
+ }
+ throw "Attempting to run auction unexpectedly did not throw"`);
+}, 'Run auction in cross-origin iframe with run-ad-auction permission denied.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ // Join an interest group owned by the the main frame's origin.
+ await joinInterestGroup(test, uuid);
+
+ let iframe = await createIframe(test, OTHER_ORIGIN1, `run-ad-auction ${OTHER_ORIGIN1}`);
+
+ await runInFrame(
+ test, iframe,
+ `await runBasicFledgeTestExpectingWinner(
+ test_instance, "${uuid}",
+ { interestGroupBuyers: ["${window.location.origin}"],
+ seller: "${OTHER_ORIGIN2}",
+ decisionLogicURL: createDecisionScriptURL("${uuid}", {origin: "${OTHER_ORIGIN2}"})
+ });`);
+}, 'Run auction in cross-origin iframe with run-ad-auction for iframe origin, which is different from seller origin.');
+
+////////////////////////////////////////////////////////////////////////////////
+// Navigate fenced frame iframe tests.
+////////////////////////////////////////////////////////////////////////////////
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ // Join an interest group and run an auction with a winner.
+ await joinInterestGroup(test, uuid);
+ let config = await runBasicFledgeTestExpectingWinner(test, uuid);
+
+ // Try to navigate a fenced frame to the winning ad in a cross-origin iframe
+ // with no fledge-related permissions.
+ let iframe = await createIframe(
+ test, OTHER_ORIGIN1, "join-ad-interest-group 'none'; run-ad-auction 'none'");
+ await runInFrame(
+ test, iframe,
+ `await createAndNavigateFencedFrame(test_instance, param);`,
+ /*param=*/config);
+ await waitForObservedRequests(
+ uuid, [createBidderReportURL(uuid), createSellerReportURL(uuid)]);
+}, 'Run auction main frame, open winning ad in cross-origin iframe.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ let iframe = await createIframe(
+ test, OTHER_ORIGIN1, "join-ad-interest-group; run-ad-auction");
+ await runInFrame(
+ test, iframe,
+ `await joinInterestGroup(test_instance, "${uuid}");
+ await runBasicFledgeAuctionAndNavigate(test_instance, "${uuid}");
+ await waitForObservedRequests(
+ "${uuid}", [createBidderReportURL("${uuid}"), createSellerReportURL("${uuid}")])`);
+}, 'Run auction in cross-origin iframe and open winning ad in nested fenced frame.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ // Run an auction in an cross-origin iframe, and get the resulting FencedFrameConfig.
+ let iframe = await createIframe(
+ test, OTHER_ORIGIN1, "join-ad-interest-group; run-ad-auction");
+ let config = await runInFrame(
+ test, iframe,
+ `await joinInterestGroup(test_instance, "${uuid}");
+ let config = await runBasicFledgeTestExpectingWinner(test_instance, "${uuid}");
+ return {result: "success", returnValue: config};`);
+ assert_true(config != null, "Value not returned from auction in iframe");
+ assert_true(config instanceof FencedFrameConfig,
+ `Wrong value type returned from auction: ${config.constructor.type}`);
+
+ // Loading the winning ad in a fenced frame that's a child of the main frame should
+ // succeed.
+ await createAndNavigateFencedFrame(test, config);
+ await waitForObservedRequests(
+ uuid,
+ [ createBidderReportURL(uuid, '1', OTHER_ORIGIN1),
+ createSellerReportURL(uuid, '1', OTHER_ORIGIN1)]);
+}, 'Run auction in cross-origin iframe and open winning ad in a fenced frame child of the main frame.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ // Run an auction in an cross-origin iframe, and get the resulting FencedFrameConfig.
+ let iframe = await createIframe(
+ test, OTHER_ORIGIN1, "join-ad-interest-group; run-ad-auction");
+ let config = await runInFrame(
+ test, iframe,
+ `await joinInterestGroup(test_instance, "${uuid}");
+ let config = await runBasicFledgeTestExpectingWinner(test_instance, "${uuid}");
+ return {result: "success", returnValue: config};`);
+ assert_true(config != null, "Value not returned from auction in iframe");
+ assert_true(config instanceof FencedFrameConfig,
+ `Wrong value type returned from auction: ${config.constructor.type}`);
+
+ // Try to navigate a fenced frame to the winning ad in a cross-origin iframe
+ // with no fledge-related permissions. The iframe is a different origin from the
+ // first cross-origin iframe.
+ let iframe2 = await createIframe(
+ test, OTHER_ORIGIN2, "join-ad-interest-group 'none'; run-ad-auction 'none'");
+ await runInFrame(
+ test, iframe2,
+ `await createAndNavigateFencedFrame(test_instance, param);`,
+ /*param=*/config);
+ await waitForObservedRequests(
+ uuid,
+ [ createBidderReportURL(uuid, '1', OTHER_ORIGIN1),
+ createSellerReportURL(uuid, '1', OTHER_ORIGIN1)]);
+}, 'Run auction in cross-origin iframe and open winning ad in a fenced frame child of another cross-origin iframe.');
+
+////////////////////////////////////////////////////////////////////////////////
+// Other tests.
+////////////////////////////////////////////////////////////////////////////////
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ let iframe = await createIframe(test, OTHER_ORIGIN1, "run-ad-auction");
+
+ // Do everything in a cross-origin iframe, and make sure correct top-frame origin is used.
+ await runInFrame(
+ test, iframe,
+ `const uuid = "${uuid}";
+ const renderURL = createRenderURL(uuid, /*script=*/null, /*signalsParam=*/'hostname');
+
+ await joinInterestGroup(
+ test_instance, uuid,
+ { trustedBiddingSignalsKeys: ['hostname'],
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL,
+ ads: [{ renderURL: renderURL }],
+ biddingLogicURL: createBiddingScriptURL({
+ generateBid:
+ \`if (browserSignals.topWindowHostname !== "${document.location.hostname}")
+ throw "Wrong topWindowHostname: " + browserSignals.topWindowHostname;
+ if (trustedBiddingSignals.hostname !== '${window.location.hostname}')
+ throw 'Wrong hostname: ' + trustedBiddingSignals.hostname;\`})});
+
+ await runBasicFledgeTestExpectingWinner(
+ test_instance, uuid,
+ { trustedScoringSignalsURL: TRUSTED_SCORING_SIGNALS_URL,
+ decisionLogicURL:
+ createDecisionScriptURL(
+ uuid,
+ { scoreAd:
+ \`if (browserSignals.topWindowHostname !== "${document.location.hostname}")
+ throw "Wrong topWindowHostname: " + browserSignals.topWindowHostname;
+ if (trustedScoringSignals.renderURL["\${renderURL}"] !== '${window.location.hostname}')
+ throw 'Wrong hostname: ' + trustedScoringSignals.renderURL["\${renderURL}"];\` })});`);
+}, 'Different top-frame origin.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ let bidderOrigin = OTHER_ORIGIN1;
+ let sellerOrigin = OTHER_ORIGIN2;
+ let bidderSendReportToURL = createBidderReportURL(uuid, '1', OTHER_ORIGIN3);
+ let sellerSendReportToURL = createSellerReportURL(uuid, '2', OTHER_ORIGIN4);
+ let bidderBeaconURL = createBidderBeaconURL(uuid, '3', OTHER_ORIGIN5);
+ let sellerBeaconURL = createSellerBeaconURL(uuid, '4', OTHER_ORIGIN6);
+ let renderURL = createRenderURL(
+ uuid,
+ `window.fence.reportEvent({
+ eventType: "beacon",
+ eventData: window.location.href,
+ destination: ["buyer", "seller"]
+ })`,
+ /*signalsParams=*/null, OTHER_ORIGIN7);
+
+ let iframe = await createIframe(test, bidderOrigin, "join-ad-interest-group");
+ let interestGroup = createInterestGroupForOrigin(
+ uuid, bidderOrigin,
+ {biddingLogicURL: createBiddingScriptURL(
+ { origin: bidderOrigin,
+ generateBid: `if (browserSignals.topWindowHostname !== "${document.location.hostname}")
+ throw "Wrong topWindowHostname: " + browserSignals.topWindowHostname;
+ if (interestGroup.owner !== "${bidderOrigin}")
+ throw "Wrong origin: " + interestGroup.owner;
+ if (!interestGroup.biddingLogicURL.startsWith("${bidderOrigin}"))
+ throw "Wrong origin: " + interestGroup.biddingLogicURL;
+ if (interestGroup.ads[0].renderUrl != "${renderURL}")
+ throw "Wrong renderURL: " + interestGroup.ads[0].renderUrl;
+ if (browserSignals.seller !== "${sellerOrigin}")
+ throw "Wrong origin: " + browserSignals.seller;`,
+ reportWin: `if (browserSignals.topWindowHostname !== "${document.location.hostname}")
+ throw "Wrong topWindowHostname: " + browserSignals.topWindowHostname;
+ if (browserSignals.seller !== "${sellerOrigin}")
+ throw "Wrong seller: " + browserSignals.seller;
+ if (browserSignals.interestGroupOwner !== "${bidderOrigin}")
+ throw "Wrong interestGroupOwner: " + browserSignals.interestGroupOwner;
+ if (browserSignals.renderURL !== "${renderURL}")
+ throw "Wrong renderURL: " + browserSignals.renderURL;
+ if (browserSignals.seller !== "${sellerOrigin}")
+ throw "Wrong seller: " + browserSignals.seller;
+ sendReportTo("${bidderSendReportToURL}");
+ registerAdBeacon({beacon: "${bidderBeaconURL}"});` }),
+ ads: [{ renderURL: renderURL }]});
+ await runInFrame(
+ test, iframe,
+ `await joinInterestGroup(test_instance, "${uuid}", ${JSON.stringify(interestGroup)});`);
+
+ await runBasicFledgeAuctionAndNavigate(test, uuid,
+ { seller: sellerOrigin,
+ interestGroupBuyers: [bidderOrigin],
+ decisionLogicURL: createDecisionScriptURL(
+ uuid,
+ { origin: sellerOrigin,
+ scoreAd: `if (browserSignals.topWindowHostname !== "${document.location.hostname}")
+ throw "Wrong topWindowHostname: " + browserSignals.topWindowHostname;
+ if (auctionConfig.seller !== "${sellerOrigin}")
+ throw "Wrong seller: " + auctionConfig.seller;
+ if (auctionConfig.interestGroupBuyers[0] !== "${bidderOrigin}")
+ throw "Wrong interestGroupBuyers: " + auctionConfig.interestGroupBuyers;
+ if (browserSignals.interestGroupOwner !== "${bidderOrigin}")
+ throw "Wrong interestGroupOwner: " + browserSignals.interestGroupOwner;
+ if (browserSignals.renderURL !== "${renderURL}")
+ throw "Wrong renderURL: " + browserSignals.renderURL;`,
+ reportResult: `if (browserSignals.topWindowHostname !== "${document.location.hostname}")
+ throw "Wrong topWindowHostname: " + browserSignals.topWindowHostname;
+ if (browserSignals.interestGroupOwner !== "${bidderOrigin}")
+ throw "Wrong interestGroupOwner: " + browserSignals.interestGroupOwner;
+ if (browserSignals.renderURL !== "${renderURL}")
+ throw "Wrong renderURL: " + browserSignals.renderURL;
+ sendReportTo("${sellerSendReportToURL}");
+ registerAdBeacon({beacon: "${sellerBeaconURL}"});`})
+ });
+
+ await waitForObservedRequests(
+ uuid,
+ [ bidderSendReportToURL,
+ sellerSendReportToURL,
+ `${bidderBeaconURL}, body: ${renderURL}`,
+ `${sellerBeaconURL}, body: ${renderURL}`
+ ]);
+}, 'Single seller auction with as many distinct origins as possible (except no component ads).');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ // Join an interest group and run an auction with a winner. Use a tracking
+ // URL for the ad, so that if it's incorrectly loaded in this test, the
+ // waitForObservedRequests() at the end of the test will see it, and the
+ // test will fail.
+ await joinInterestGroup(
+ test, uuid,
+ {ads: [{renderURL: createTrackerURL(window.location.origin, uuid, 'track_get', 'renderURL')}]});
+ let config = await runBasicFledgeTestExpectingWinner(test, uuid);
+
+ // Try to navigate a fenced frame to the winning ad in a new same-origin
+ // window. This should fail. Unfortunately, there's no assertion that
+ // can be checked for, and can't communicate with the contents of the
+ // fenced frame to make sure the load fails.
+ //
+ // So instead, join an interest group with a different sendReportTo-url,
+ // overwriting the previously joined one, and run another auction, loading
+ // the winner in another fenced frame.
+ //
+ // Then wait to see that only the reporting URLs from that second auction
+ // are requested. They should almost always be requested after the URLs
+ // from the first auction.
+ let child_window =
+ await createFrame(test, document.location.origin, /*is_iframe=*/false);
+ await runInFrame(
+ test, child_window,
+ `await createAndNavigateFencedFrame(test_instance, param);
+ await joinInterestGroup(
+ test_instance, "${uuid}",
+ {biddingLogicURL: createBiddingScriptURL(
+ {reportWin: "sendReportTo('${createBidderReportURL(uuid, "2")}');" })});
+ await runBasicFledgeAuctionAndNavigate(test_instance, "${uuid}");`,
+ /*param=*/config);
+ await waitForObservedRequests(
+ uuid, [createBidderReportURL(uuid, "2"), createSellerReportURL(uuid)]);
+}, 'Run auction in main frame, try to open winning ad in different same-origin main frame.');
diff --git a/testing/web-platform/tests/fledge/tentative/currency.https.window.js b/testing/web-platform/tests/fledge/tentative/currency.https.window.js
new file mode 100644
index 0000000000..9a33d12148
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/currency.https.window.js
@@ -0,0 +1,874 @@
+// META: script=/resources/testdriver.js
+// META: script=/common/utils.js
+// META: script=resources/fledge-util.sub.js
+// META: script=/common/subset-tests.js
+// META: timeout=long
+// META: variant=?1-4
+// META: variant=?5-8
+// META: variant=?9-12
+// META: variant=?13-16
+// META: variant=?17-20
+// META: variant=?21-24
+// META: variant=?25-28
+// META: variant=?29-32
+// META: variant=?33-last
+
+'use strict;'
+
+const ORIGIN = window.location.origin;
+
+// The tests in this file focus on calls to runAdAuction involving currency
+// handling.
+
+// Joins an interest group that bids 9USD on window.location.origin, and one
+// that bids 10CAD on OTHER_ORIGIN1, each with a reportWin() report.
+async function joinTwoCurrencyGroups(test, uuid) {
+ const reportWinURL = createBidderReportURL(uuid, 'USD');
+ const biddingURL = createBiddingScriptURL(
+ {bidCurrency: 'USD', reportWin: `sendReportTo('${reportWinURL}')`});
+ await joinInterestGroup(test, uuid, {biddingLogicURL: biddingURL});
+
+ const otherReportWinURL = createBidderReportURL(uuid, 'CAD', OTHER_ORIGIN1);
+ const otherBiddingURL = createBiddingScriptURL({
+ origin: OTHER_ORIGIN1,
+ bid: 10,
+ bidCurrency: 'CAD',
+ reportWin: `sendReportTo('${otherReportWinURL}')`
+ });
+ await joinCrossOriginInterestGroup(
+ test, uuid, OTHER_ORIGIN1, {biddingLogicURL: otherBiddingURL});
+}
+
+function createBiddingScriptURLWithCurrency(uuid, currency) {
+ return createBiddingScriptURL({
+ bidCurrency: currency,
+ allowComponentAuction: true,
+ reportWin: `
+ sendReportTo('${createBidderReportURL(uuid, /*id=*/ '')}' +
+ browserSignals.bid + browserSignals.bidCurrency);`,
+ });
+}
+
+// Creates a component-auction eligible bidding script returning a bid `bid` in
+// currency `currency`. It provides a reporting handler that logs bid and
+// highestScoringOtherBid along with their currencies.
+function createBiddingScriptURLForHighestScoringOther(uuid, bid, currency) {
+ return createBiddingScriptURL({
+ bid: bid,
+ bidCurrency: currency,
+ allowComponentAuction: true,
+ generateBid: `
+ forDebuggingOnly.reportAdAuctionWin(
+ '${createBidderReportURL(uuid, /*id=*/ 'dbg_')}' +
+ '\${winningBid}\${winningBidCurrency}_' +
+ '\${highestScoringOtherBid}\${highestScoringOtherBidCurrency}');`,
+ reportWin: `
+ sendReportTo(
+ '${createBidderReportURL(uuid, /*id=*/ '')}' +
+ browserSignals.bid + browserSignals.bidCurrency +
+ '_' + browserSignals.highestScoringOtherBid +
+ browserSignals.highestScoringOtherBidCurrency);`,
+ });
+}
+
+function createDecisionURLExpectCurrency(uuid, currencyInScore) {
+ return createDecisionScriptURL(uuid, {
+ scoreAd: `
+ if (browserSignals.bidCurrency !== '${currencyInScore}')
+ throw 'Wrong currency';`,
+ reportResult: `
+ sendReportTo('${createSellerReportURL(uuid, /*id=*/ '')}' +
+ browserSignals.bid + browserSignals.bidCurrency);`,
+ });
+}
+
+// Creates a component-auction seller script, which by default just scores
+// bid * 2, but the `conversion` argument can be used to customize bid
+// modification and currenct conversion.
+//
+// The script provides a reporting handler that logs bid and
+// highestScoringOtherBid along with their currencies as well as `suffix`.
+function createDecisionURLForHighestScoringOther(
+ uuid, conversion = '', suffix = '') {
+ return createDecisionScriptURL(uuid, {
+ scoreAd: `
+ forDebuggingOnly.reportAdAuctionWin(
+ '${createSellerReportURL(uuid, /*id=*/ 'dbg_')}' + '${suffix}' +
+ '\${winningBid}\${winningBidCurrency}_' +
+ '\${highestScoringOtherBid}\${highestScoringOtherBidCurrency}');
+ let converted = undefined;
+ let modified = undefined;
+ let modifiedCurrency = undefined;
+ ${conversion}
+ return {desirability: 2 * bid,
+ incomingBidInSellerCurrency: converted,
+ bid: modified,
+ bidCurrency: modifiedCurrency,
+ allowComponentAuction: true};
+ `,
+ reportResult: `
+ sendReportTo(
+ '${createSellerReportURL(uuid, /*id=*/ '')}' + '${suffix}' +
+ browserSignals.bid + browserSignals.bidCurrency +
+ '_' + browserSignals.highestScoringOtherBid +
+ browserSignals.highestScoringOtherBidCurrency);`,
+ });
+}
+
+// Joins groups for 9USD and 10USD, with reporting including
+// highestScoringOtherBid.
+async function joinTwoGroupsForHighestScoringOther(test, uuid) {
+ await joinInterestGroup(test, uuid, {
+ name: 'group-9USD',
+ biddingLogicURL:
+ createBiddingScriptURLForHighestScoringOther(uuid, /*bid=*/ 9, 'USD')
+ });
+ await joinInterestGroup(test, uuid, {
+ name: 'group-10USD',
+ biddingLogicURL:
+ createBiddingScriptURLForHighestScoringOther(uuid, /*bid=*/ 10, 'USD')
+ });
+}
+
+async function runCurrencyComponentAuction(test, uuid, params = {}) {
+ let auctionConfigOverrides = {
+ interestGroupBuyers: [],
+ decisionLogicURL: createDecisionScriptURL(uuid, {
+ reportResult: `
+ sendReportTo('${createSellerReportURL(uuid, 'top_')}' +
+ browserSignals.bid + browserSignals.bidCurrency)`,
+ ...params.topLevelSellerScriptParamsOverride
+ }),
+ componentAuctions: [{
+ seller: ORIGIN,
+ decisionLogicURL: createDecisionScriptURL(uuid, {
+ reportResult: `
+ sendReportTo('${createSellerReportURL(uuid, 'component_')}' +
+ browserSignals.bid + browserSignals.bidCurrency)`,
+ ...params.componentSellerScriptParamsOverride
+ }),
+ interestGroupBuyers: [ORIGIN],
+ ...params.componentAuctionConfigOverrides
+ }],
+ ...params.topLevelAuctionConfigOverrides
+ };
+ return await runBasicFledgeAuction(test, uuid, auctionConfigOverrides);
+}
+
+// Runs a component auction with reporting scripts that report bid and
+// highestScoringOtherBid, along with their currencies.
+//
+// Customization points in `params` are:
+// componentAuctionConfigOverrides, topLevelAuctionConfigOverrides:
+// edit auctionConfig for given auction level.
+//
+// topLevelConversion and componentConversion:
+// Permit customizing how the scoring function does currency conversiona and
+// bid modification. See createDecisionURLForHighestScoringOther().
+async function runCurrencyComponentAuctionForHighestScoringOther(
+ test, uuid, params = {}) {
+ let auctionConfigOverrides = {
+ interestGroupBuyers: [],
+ decisionLogicURL: createDecisionURLForHighestScoringOther(
+ uuid, params.topLevelConversion || '', 'top_'),
+ componentAuctions: [{
+ seller: ORIGIN,
+ decisionLogicURL: createDecisionURLForHighestScoringOther(
+ uuid, params.componentConversion || '', 'component_'),
+ interestGroupBuyers: [ORIGIN],
+ ...params.componentAuctionConfigOverrides
+ }],
+ ...params.topLevelAuctionConfigOverrides
+ };
+ return await runBasicFledgeAuction(test, uuid, auctionConfigOverrides);
+}
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await joinInterestGroup(
+ test, uuid,
+ {biddingLogicURL: createBiddingScriptURL({bidCurrency: 'usd'})});
+ await runBasicFledgeTestExpectingNoWinner(test, uuid);
+}, 'Returning bid with invalid currency.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await joinInterestGroup(
+ test, uuid,
+ {biddingLogicURL: createBiddingScriptURLWithCurrency(uuid, 'USD')});
+ await runBasicFledgeAuctionAndNavigate(
+ test, uuid,
+ {decisionLogicURL: createDecisionURLExpectCurrency(uuid, 'USD')});
+ await waitForObservedRequests(uuid, [
+ createSellerReportURL(uuid, '9???'), createBidderReportURL(uuid, '9???')
+ ]);
+}, 'Returning bid with currency, configuration w/o currency.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await joinInterestGroup(
+ test, uuid,
+ {biddingLogicURL: createBiddingScriptURLWithCurrency(uuid, undefined)});
+ await runBasicFledgeAuctionAndNavigate(test, uuid, {
+ perBuyerCurrencies: {'*': 'USD'},
+ decisionLogicURL: createDecisionURLExpectCurrency(uuid, '???')
+ });
+ await waitForObservedRequests(uuid, [
+ createSellerReportURL(uuid, '9USD'), createBidderReportURL(uuid, '9USD')
+ ]);
+}, 'Returning bid w/o currency, configuration w/currency.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await joinInterestGroup(
+ test, uuid,
+ {biddingLogicURL: createBiddingScriptURLWithCurrency(uuid, 'USD')});
+ await runBasicFledgeAuctionAndNavigate(test, uuid, {
+ perBuyerCurrencies: {'*': 'USD'},
+ decisionLogicURL: createDecisionURLExpectCurrency(uuid, 'USD')
+ });
+ await waitForObservedRequests(uuid, [
+ createSellerReportURL(uuid, '9USD'), createBidderReportURL(uuid, '9USD')
+ ]);
+}, 'Returning bid w/currency, configuration w/matching currency.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await joinInterestGroup(
+ test, uuid,
+ {biddingLogicURL: createBiddingScriptURL({bidCurrency: 'USD'})});
+ await runBasicFledgeTestExpectingNoWinner(
+ test, uuid, {perBuyerCurrencies: {'*': 'CAD'}});
+}, 'Returning bid w/currency, configuration w/different currency.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await joinTwoCurrencyGroups(test, uuid);
+ let auctionConfigOverrides = {
+ interestGroupBuyers: [ORIGIN, OTHER_ORIGIN1],
+ perBuyerCurrencies: {}
+ };
+ auctionConfigOverrides.perBuyerCurrencies['*'] = 'USD';
+ auctionConfigOverrides.perBuyerCurrencies[OTHER_ORIGIN1] = 'CAD';
+ await runBasicFledgeAuctionAndNavigate(test, uuid, auctionConfigOverrides);
+
+ // Since the scoring script doesn't actually look at the currencies,
+ // We expect 10CAD to win because 10 > 9
+ await waitForObservedRequests(uuid, [
+ createBidderReportURL(uuid, 'CAD', OTHER_ORIGIN1),
+ createSellerReportURL(uuid)
+ ]);
+}, 'Different currencies for different origins, all match.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await joinTwoCurrencyGroups(test, uuid);
+ let auctionConfigOverrides = {
+ interestGroupBuyers: [ORIGIN, OTHER_ORIGIN1],
+ perBuyerCurrencies: {}
+ };
+ auctionConfigOverrides.perBuyerCurrencies[ORIGIN] = 'USD';
+ auctionConfigOverrides.perBuyerCurrencies[OTHER_ORIGIN1] = 'EUR';
+ await runBasicFledgeAuctionAndNavigate(test, uuid, auctionConfigOverrides);
+
+ // Since the configuration for CAD script expects EUR only the USD bid goes
+ // through.
+ await waitForObservedRequests(
+ uuid, [createBidderReportURL(uuid, 'USD'), createSellerReportURL(uuid)]);
+}, 'Different currencies for different origins, USD one matches.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await joinTwoCurrencyGroups(test, uuid);
+ let auctionConfigOverrides = {
+ interestGroupBuyers: [ORIGIN, OTHER_ORIGIN1],
+ perBuyerCurrencies: {}
+ };
+ auctionConfigOverrides.perBuyerCurrencies['*'] = 'EUR';
+}, 'Different currencies for different origins, none match.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await joinInterestGroup(
+ test, uuid,
+ {biddingLogicURL: createBiddingScriptURLWithCurrency(uuid, 'USD')});
+ let config = await runCurrencyComponentAuction(test, uuid, {
+ topLevelSellerScriptParamsOverride: {
+ scoreAd: `
+ if (browserSignals.bidCurrency !== 'USD')
+ throw 'Wrong currency';`
+ }
+ });
+ expectSuccess(config);
+ createAndNavigateFencedFrame(test, config);
+ // While scoring sees the original currency tag, reporting currency tags are
+ // config-based.
+ await waitForObservedRequests(uuid, [
+ createSellerReportURL(uuid, 'top_9???'),
+ createSellerReportURL(uuid, 'component_9???'),
+ createBidderReportURL(uuid, '9???')
+ ]);
+}, 'Multi-seller auction --- no currency restriction.');
+
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await joinInterestGroup(
+ test, uuid,
+ {biddingLogicURL: createBiddingScriptURLWithCurrency(uuid, 'USD')});
+ let config = await runCurrencyComponentAuction(test, uuid, {
+ componentAuctionConfigOverrides: {sellerCurrency: 'USD'},
+ topLevelSellerScriptParamsOverride: {
+ scoreAd: `
+ if (browserSignals.bidCurrency !== 'USD')
+ throw 'Wrong currency';`
+ }
+ });
+ expectSuccess(config);
+ createAndNavigateFencedFrame(test, config);
+ // Because component's sellerCurrency is USD, the bid it makes is seen to be
+ // in dollars by top-level reporting. That doesn't affect reporting in its
+ // own auction.
+ await waitForObservedRequests(uuid, [
+ createSellerReportURL(uuid, 'top_9USD'),
+ createSellerReportURL(uuid, 'component_9???'),
+ createBidderReportURL(uuid, '9???')
+ ]);
+}, 'Multi-seller auction --- component sellerCurrency matches bid.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await joinInterestGroup(
+ test, uuid,
+ {biddingLogicURL: createBiddingScriptURLWithCurrency(uuid, 'USD')});
+ let config = await runCurrencyComponentAuction(test, uuid, {
+ componentAuctionConfigOverrides: {sellerCurrency: 'EUR'},
+ componentSellerScriptParamsOverride: {
+ scoreAd: `
+ return {desirability: 2 * bid, allowComponentAuction: true,
+ bid: 1.5 * bid, bidCurrency: 'EUR'}
+ `
+ },
+ topLevelSellerScriptParamsOverride: {
+ scoreAd: `
+ if (browserSignals.bidCurrency !== 'EUR')
+ throw 'Wrong currency';`
+ }
+ });
+ expectSuccess(config);
+ createAndNavigateFencedFrame(test, config);
+ // Because component's sellerCurrency is USD, the bid it makes is seen to be
+ // in dollars by top-level reporting. That doesn't affect reporting in its
+ // own auction.
+ await waitForObservedRequests(uuid, [
+ createSellerReportURL(uuid, 'top_13.5EUR'),
+ createSellerReportURL(uuid, 'component_9???'),
+ createBidderReportURL(uuid, '9???')
+ ]);
+}, 'Multi-seller auction --- component scoreAd modifies bid into its sellerCurrency.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await joinInterestGroup(
+ test, uuid,
+ {biddingLogicURL: createBiddingScriptURLWithCurrency(uuid, 'USD')});
+ let config = await runCurrencyComponentAuction(test, uuid, {
+ componentAuctionConfigOverrides: {sellerCurrency: 'EUR'},
+ componentSellerScriptParamsOverride: {
+ scoreAd: `
+ return {desirability: 2 * bid, allowComponentAuction: true,
+ bid: 1.5 * bid}
+ `
+ },
+ topLevelSellerScriptParamsOverride: {
+ scoreAd: `
+ // scoreAd sees what's actually passed in.
+ if (browserSignals.bidCurrency !== '???')
+ throw 'Wrong currency';`
+ }
+ });
+ expectSuccess(config);
+ createAndNavigateFencedFrame(test, config);
+ await waitForObservedRequests(uuid, [
+ createSellerReportURL(uuid, 'top_13.5EUR'),
+ createSellerReportURL(uuid, 'component_9???'),
+ createBidderReportURL(uuid, '9???')
+ ]);
+}, 'Multi-seller auction --- component scoreAd modifies bid, no explicit currency.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await joinInterestGroup(
+ test, uuid,
+ {biddingLogicURL: createBiddingScriptURLWithCurrency(uuid, 'USD')});
+ let config = await runCurrencyComponentAuction(test, uuid, {
+ componentAuctionConfigOverrides:
+ {sellerCurrency: 'EUR', perBuyerCurrencies: {'*': 'USD'}},
+ componentSellerScriptParamsOverride: {
+ scoreAd: `
+ return {desirability: 2 * bid, allowComponentAuction: true,
+ bid: 1.5 * bid}
+ `
+ },
+ topLevelSellerScriptParamsOverride: {
+ scoreAd: `
+ // scoreAd sees what's actually passed in.
+ if (browserSignals.bidCurrency !== '???')
+ throw 'Wrong currency';`
+ }
+ });
+ expectSuccess(config);
+ createAndNavigateFencedFrame(test, config);
+ await waitForObservedRequests(uuid, [
+ createSellerReportURL(uuid, 'top_13.5EUR'),
+ createSellerReportURL(uuid, 'component_9USD'),
+ createBidderReportURL(uuid, '9USD')
+ ]);
+}, 'Multi-seller auction --- component scoreAd modifies bid, bidder has bidCurrency.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await joinInterestGroup(
+ test, uuid,
+ {biddingLogicURL: createBiddingScriptURLWithCurrency(uuid, 'USD')});
+ let config = await runCurrencyComponentAuction(test, uuid, {
+ componentAuctionConfigOverrides: {perBuyerCurrencies: {'*': 'USD'}},
+ componentSellerScriptParamsOverride: {
+ scoreAd: `
+ return {desirability: 2 * bid, allowComponentAuction: true,
+ bid: 1.5 * bid}
+ `
+ },
+ topLevelSellerScriptParamsOverride: {
+ scoreAd: `
+ // scoreAd sees what's actually passed in.
+ if (browserSignals.bidCurrency !== '???')
+ throw 'Wrong currency';`
+ }
+ });
+ expectSuccess(config);
+ createAndNavigateFencedFrame(test, config);
+ await waitForObservedRequests(uuid, [
+ createSellerReportURL(uuid, 'top_13.5???'),
+ createSellerReportURL(uuid, 'component_9USD'),
+ createBidderReportURL(uuid, '9USD')
+ ]);
+}, 'Multi-seller auction --- only bidder currency specified.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await joinInterestGroup(
+ test, uuid,
+ {biddingLogicURL: createBiddingScriptURLWithCurrency(uuid, 'USD')});
+ let config = await runCurrencyComponentAuction(test, uuid, {
+ componentAuctionConfigOverrides: {perBuyerCurrencies: {'*': 'USD'}},
+ componentSellerScriptParamsOverride: {
+ scoreAd: `
+ return {desirability: 2 * bid, allowComponentAuction: true,
+ bid: 1.5 * bid, bidCurrency: 'CAD'}
+ `
+ },
+ topLevelSellerScriptParamsOverride: {
+ scoreAd: `
+ // scoreAd sees what's actually passed in.
+ if (browserSignals.bidCurrency !== 'CAD')
+ throw 'Wrong currency';`
+ }
+ });
+ expectSuccess(config);
+ createAndNavigateFencedFrame(test, config);
+ await waitForObservedRequests(uuid, [
+ createSellerReportURL(uuid, 'top_13.5???'),
+ createSellerReportURL(uuid, 'component_9USD'),
+ createBidderReportURL(uuid, '9USD')
+ ]);
+}, 'Multi-seller auction --- only bidder currency in config, component uses explicit currency.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await joinInterestGroup(test, uuid, {
+ biddingLogicURL:
+ createBiddingScriptURLWithCurrency(uuid, /*bidCurrency=*/ undefined)
+ });
+ let config = await runCurrencyComponentAuction(test, uuid, {
+ componentAuctionConfigOverrides: {sellerCurrency: 'CAD'},
+ componentSellerScriptParamsOverride: {
+ scoreAd: `
+ return {desirability: 2 * bid, allowComponentAuction: true,
+ incomingBidInSellerCurrency: 12345}
+ `
+ },
+ topLevelSellerScriptParamsOverride: {
+ scoreAd: `
+ // scoreAd sees what's actually passed in.
+ if (bid != 9)
+ throw 'Wrong bid';
+ if (browserSignals.bidCurrency !== '???')
+ throw 'Wrong currency';`
+ }
+ });
+ expectSuccess(config);
+ createAndNavigateFencedFrame(test, config);
+ await waitForObservedRequests(uuid, [
+ createSellerReportURL(uuid, 'top_9CAD'),
+ createSellerReportURL(uuid, 'component_9???'),
+ createBidderReportURL(uuid, '9???')
+ ]);
+}, 'Multi-seller auction --- incomingBidInSellerCurrency does not go to top-level; component sellerCurrency does.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await joinInterestGroup(
+ test, uuid,
+ {biddingLogicURL: createBiddingScriptURLWithCurrency(uuid, 'USD')});
+ let result = await runCurrencyComponentAuction(test, uuid, {
+ componentAuctionConfigOverrides: {sellerCurrency: 'EUR'},
+ componentSellerScriptParamsOverride: {
+ scoreAd: `
+ return {desirability: 2 * bid, allowComponentAuction: true,
+ bid: 1.5 * bid, bidCurrency: 'CAD'}
+ `
+ }
+ });
+ expectNoWinner(result);
+}, 'Multi-seller auction --- component scoreAd modifies bid to wrong currency.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await joinInterestGroup(
+ test, uuid,
+ {biddingLogicURL: createBiddingScriptURLWithCurrency(uuid, 'USD')});
+ let topLevelConfigOverride = {perBuyerCurrencies: {}};
+ topLevelConfigOverride.perBuyerCurrencies[ORIGIN] = 'USD';
+ let config = await runCurrencyComponentAuction(test, uuid, {
+ topLevelAuctionConfigOverrides: topLevelConfigOverride,
+ topLevelSellerScriptParamsOverride: {
+ scoreAd: `
+ if (browserSignals.bidCurrency !== 'USD')
+ throw 'Wrong currency';`
+ }
+ });
+ expectSuccess(config);
+ createAndNavigateFencedFrame(test, config);
+ // Because component is constrained by perBuyerCurrencies for it on top-level
+ // to USD, the bid it makes is seen to be in dollars by top-level reporting.
+ // That doesn't affect reporting in its own auction.
+ await waitForObservedRequests(uuid, [
+ createSellerReportURL(uuid, 'top_9USD'),
+ createSellerReportURL(uuid, 'component_9???'),
+ createBidderReportURL(uuid, '9???')
+ ]);
+}, 'Multi-seller auction --- top-level perBuyerCurrencies matches bid.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await joinInterestGroup(
+ test, uuid,
+ {biddingLogicURL: createBiddingScriptURLWithCurrency(uuid, 'USD')});
+ let topLevelConfigOverride = {perBuyerCurrencies: {}};
+ topLevelConfigOverride.perBuyerCurrencies[ORIGIN] = 'USD';
+ let config = await runCurrencyComponentAuction(test, uuid, {
+ componentAuctionConfigOverrides: {sellerCurrency: 'USD'},
+ topLevelAuctionConfigOverrides: topLevelConfigOverride,
+ topLevelSellerScriptParamsOverride: {
+ scoreAd: `
+ if (browserSignals.bidCurrency !== 'USD')
+ throw 'Wrong currency';`
+ }
+ });
+ expectSuccess(config);
+ createAndNavigateFencedFrame(test, config);
+ // Because component is constrained by perBuyerCurrencies for it on top-level
+ // to USD, the bid it makes is seen to be in dollars by top-level reporting.
+ // That doesn't affect reporting in its own auction.
+ await waitForObservedRequests(uuid, [
+ createSellerReportURL(uuid, 'top_9USD'),
+ createSellerReportURL(uuid, 'component_9???'),
+ createBidderReportURL(uuid, '9???')
+ ]);
+}, 'Multi-seller auction --- consistent sellerConfig and top-level perBuyerCurrencies.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await joinInterestGroup(
+ test, uuid,
+ {biddingLogicURL: createBiddingScriptURLWithCurrency(uuid, 'USD')});
+ let topLevelConfigOverride = {perBuyerCurrencies: {}};
+ topLevelConfigOverride.perBuyerCurrencies[ORIGIN] = 'EUR';
+ let result = await runCurrencyComponentAuction(test, uuid, {
+ componentAuctionConfigOverrides: {sellerCurrency: 'USD'},
+ topLevelAuctionConfigOverrides: topLevelConfigOverride,
+ topLevelSellerScriptParamsOverride: {
+ scoreAd: `
+ if (browserSignals.bidCurrency !== 'USD')
+ throw 'Wrong currency';`
+ }
+ });
+ expectNoWinner(result);
+}, 'Multi-seller auction --- inconsistent sellerConfig and top-level perBuyerCurrencies.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await joinInterestGroup(
+ test, uuid,
+ {biddingLogicURL: createBiddingScriptURLWithCurrency(uuid, 'USD')});
+ let topLevelConfigOverride = {perBuyerCurrencies: {}};
+ topLevelConfigOverride.perBuyerCurrencies[ORIGIN] = 'EUR';
+
+ let result = await runCurrencyComponentAuction(
+ test, uuid, {componentAuctionConfigOverrides: topLevelConfigOverride});
+ expectNoWinner(result);
+}, 'Multi-seller auction --- top-level perBuyerCurrencies different from bid.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await joinInterestGroup(
+ test, uuid,
+ {biddingLogicURL: createBiddingScriptURLWithCurrency(uuid, 'USD')});
+ let result = await runCurrencyComponentAuction(
+ test, uuid, {componentAuctionConfigOverrides: {sellerCurrency: 'EUR'}});
+ expectNoWinner(result);
+}, 'Multi-seller auction --- component sellerCurrency different from bid.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await joinInterestGroup(test, uuid);
+ await runBasicFledgeTestExpectingNoWinner(test, uuid, {
+ decisionLogicURL: createDecisionScriptURL(uuid, {
+ scoreAd: `
+ return {desirability: 2 * bid,
+ incomingBidInSellerCurrency: 5* bid}
+ `
+ })
+ });
+}, 'Trying to use incomingBidInSellerCurrency w/o sellerCurrency set.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await joinInterestGroup(test, uuid);
+ await runBasicFledgeTestExpectingWinner(test, uuid, {
+ decisionLogicURL: createDecisionScriptURL(uuid, {
+ scoreAd: `
+ return {desirability: 2 * bid,
+ incomingBidInSellerCurrency: 5* bid}
+ `,
+ }),
+ sellerCurrency: 'USD'
+ });
+}, 'Trying to use incomingBidInSellerCurrency w/sellerCurrency set.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await joinInterestGroup(
+ test, uuid,
+ {biddingLogicURL: createBiddingScriptURLWithCurrency(uuid, 'USD')});
+ await runBasicFledgeTestExpectingNoWinner(test, uuid, {
+ decisionLogicURL: createDecisionScriptURL(uuid, {
+ scoreAd: `
+ return {desirability: 2 * bid,
+ incomingBidInSellerCurrency: 5* bid}
+ `
+ }),
+ sellerCurrency: 'USD'
+ });
+}, 'Trying to use incomingBidInSellerCurrency to change bid already in that currency.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await joinInterestGroup(
+ test, uuid,
+ {biddingLogicURL: createBiddingScriptURLWithCurrency(uuid, 'USD')});
+ await runBasicFledgeTestExpectingWinner(test, uuid, {
+ decisionLogicURL: createDecisionScriptURL(uuid, {
+ scoreAd: `
+ return {desirability: 2 * bid,
+ incomingBidInSellerCurrency: bid}
+ `
+ }),
+ sellerCurrency: 'USD'
+ });
+}, 'incomingBidInSellerCurrency repeating value of bid already in that currency is OK.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await joinTwoGroupsForHighestScoringOther(test, uuid);
+ await runBasicFledgeAuctionAndNavigate(
+ test, uuid,
+ {decisionLogicURL: createDecisionURLForHighestScoringOther(uuid)});
+ await waitForObservedRequests(uuid, [
+ createSellerReportURL(uuid, '10???_9???'),
+ createBidderReportURL(uuid, '10???_9???'),
+ // w/o sellerCurrency set, forDebuggingOnly reports original values and ???
+ // as tags.
+ createSellerReportURL(uuid, 'dbg_10???_9???'),
+ createBidderReportURL(uuid, 'dbg_10???_9???')
+ ]);
+}, 'Converted currency use with no sellerCurrency set.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await joinTwoGroupsForHighestScoringOther(test, uuid);
+ await runBasicFledgeAuctionAndNavigate(test, uuid, {
+ decisionLogicURL: createDecisionURLForHighestScoringOther(uuid),
+ sellerCurrency: 'USD'
+ });
+ await waitForObservedRequests(uuid, [
+ createSellerReportURL(uuid, '10???_9USD'),
+ createBidderReportURL(uuid, '10???_9USD'),
+ // w/sellerCurrency set, forDebuggingOnly reports converted bids +
+ // sellerCurrency.
+ createSellerReportURL(uuid, 'dbg_10USD_9USD'),
+ createBidderReportURL(uuid, 'dbg_10USD_9USD')
+ ]);
+}, 'Converted currency use with sellerCurrency set matching.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await joinTwoGroupsForHighestScoringOther(test, uuid);
+ await runBasicFledgeAuctionAndNavigate(test, uuid, {
+ decisionLogicURL: createDecisionURLForHighestScoringOther(uuid),
+ sellerCurrency: 'EUR'
+ });
+ await waitForObservedRequests(uuid, [
+ createSellerReportURL(uuid, '10???_0EUR'),
+ createBidderReportURL(uuid, '10???_0EUR'),
+ // sellerCurrency set, and no bid available in it: get 0s.
+ createSellerReportURL(uuid, 'dbg_0EUR_0EUR'),
+ createBidderReportURL(uuid, 'dbg_0EUR_0EUR')
+ ]);
+}, 'Converted currency use with sellerCurrency different, no conversion.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await joinTwoGroupsForHighestScoringOther(test, uuid);
+ await runBasicFledgeAuctionAndNavigate(test, uuid, {
+ decisionLogicURL:
+ createDecisionURLForHighestScoringOther(uuid, 'converted = 3 * bid'),
+ sellerCurrency: 'EUR'
+ });
+ await waitForObservedRequests(uuid, [
+ createSellerReportURL(uuid, '10???_27EUR'),
+ createBidderReportURL(uuid, '10???_27EUR'),
+ // sellerCurrency set, converted bids.
+ createSellerReportURL(uuid, 'dbg_30EUR_27EUR'),
+ createBidderReportURL(uuid, 'dbg_30EUR_27EUR')
+ ]);
+}, 'Converted currency use with sellerCurrency different, conversion.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await joinTwoGroupsForHighestScoringOther(test, uuid);
+ let result =
+ await runCurrencyComponentAuctionForHighestScoringOther(test, uuid, {
+ componentConversion: `
+ modified = bid + 1;
+ modifiedCurrency = 'EUR';`,
+ componentAuctionConfigOverrides: {sellerCurrency: 'EUR'}
+ });
+ expectSuccess(result);
+ createAndNavigateFencedFrame(test, result);
+ await waitForObservedRequests(uuid, [
+ createSellerReportURL(uuid, 'top_11EUR_0???'),
+ createSellerReportURL(uuid, 'component_10???_0EUR'),
+ createBidderReportURL(uuid, '10???_0EUR'),
+ // forDebuggingOnly info w/sellerCurrency set relies on conversion;
+ // but sellerCurrency is on component auction only.
+ createBidderReportURL(uuid, 'dbg_0EUR_0EUR'),
+ createSellerReportURL(uuid, 'dbg_component_0EUR_0EUR'),
+ createSellerReportURL(uuid, 'dbg_top_11???_0???'),
+ ]);
+}, 'Modified bid does not act in place of incomingBidInSellerCurrency.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await joinTwoGroupsForHighestScoringOther(test, uuid);
+ let result =
+ await runCurrencyComponentAuctionForHighestScoringOther(test, uuid, {
+ componentConversion: `
+ modified = bid + 1;
+ modifiedCurrency = 'EUR';
+ converted = bid - 1;`,
+ componentAuctionConfigOverrides: {sellerCurrency: 'EUR'}
+ });
+ expectSuccess(result);
+ createAndNavigateFencedFrame(test, result);
+ await waitForObservedRequests(uuid, [
+ createSellerReportURL(uuid, 'top_11EUR_0???'),
+ createSellerReportURL(uuid, 'component_10???_8EUR'),
+ createBidderReportURL(uuid, '10???_8EUR'),
+ // Debug at component shows converted; top-level has no sellerCurrency,
+ // so shows modified.
+ createBidderReportURL(uuid, 'dbg_9EUR_8EUR'),
+ createSellerReportURL(uuid, 'dbg_component_9EUR_8EUR'),
+ createSellerReportURL(uuid, 'dbg_top_11???_0???'),
+ ]);
+}, 'Both modified bid and incomingBidInSellerCurrency.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await joinTwoGroupsForHighestScoringOther(test, uuid);
+ let result =
+ await runCurrencyComponentAuctionForHighestScoringOther(test, uuid, {
+ componentConversion: `
+ modified = bid + 1;
+ modifiedCurrency = 'CAD';`,
+ topLevelAuctionConfigOverrides: {sellerCurrency: 'EUR'},
+ topLevelConversion: `converted = 3 * bid;`,
+ });
+ expectSuccess(result);
+ createAndNavigateFencedFrame(test, result);
+ await waitForObservedRequests(uuid, [
+ createSellerReportURL(uuid, 'top_11???_0???'),
+ createSellerReportURL(uuid, 'component_10???_9???'),
+ createBidderReportURL(uuid, '10???_9???'),
+ // No sellerCurrency at component; debug at top-level shows the result of
+ // conversion.
+ createBidderReportURL(uuid, 'dbg_10???_9???'),
+ createSellerReportURL(uuid, 'dbg_component_10???_9???'),
+ createSellerReportURL(uuid, 'dbg_top_33EUR_0???'),
+ ]);
+}, 'incomingBidInSellerCurrency at top-level trying to convert is OK.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await joinTwoGroupsForHighestScoringOther(test, uuid);
+ let result =
+ await runCurrencyComponentAuctionForHighestScoringOther(test, uuid, {
+ componentConversion: `
+ modified = bid + 1;
+ modifiedCurrency = 'EUR';`,
+ topLevelAuctionConfigOverrides: {sellerCurrency: 'EUR'},
+ topLevelConversion: `converted = 3 * bid;`,
+ });
+ // Tried to change a bid that was already in EUR.
+ expectNoWinner(result);
+}, 'incomingBidInSellerCurrency at top-level trying to change bid is not OK.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await joinTwoGroupsForHighestScoringOther(test, uuid);
+ let result =
+ await runCurrencyComponentAuctionForHighestScoringOther(test, uuid, {
+ componentConversion: `
+ modified = bid + 1;
+ modifiedCurrency = 'EUR';`,
+ topLevelAuctionConfigOverrides: {sellerCurrency: 'EUR'},
+ topLevelConversion: `converted = bid;`,
+ });
+ // Changing the bid to itself when it was already in right currency is OK.
+ expectSuccess(result);
+ createAndNavigateFencedFrame(test, result);
+ await waitForObservedRequests(uuid, [
+ createSellerReportURL(uuid, 'top_11???_0???'),
+ createSellerReportURL(uuid, 'component_10???_9???'),
+ createBidderReportURL(uuid, '10???_9???'),
+ // No sellerCurrency at component; debug at top-level shows the result of
+ // no-op conversion.
+ createBidderReportURL(uuid, 'dbg_10???_9???'),
+ createSellerReportURL(uuid, 'dbg_component_10???_9???'),
+ createSellerReportURL(uuid, 'dbg_top_11EUR_0???'),
+ ]);
+}, 'incomingBidInSellerCurrency at top-level doing a no-op conversion OK.');
+
+// TODO: PrivateAggregation. It follows the same rules as
+// highestScoringOtherBid, but is actually visible at top-level.
diff --git a/testing/web-platform/tests/fledge/tentative/direct-from-seller-signals.https.window.js b/testing/web-platform/tests/fledge/tentative/direct-from-seller-signals.https.window.js
new file mode 100644
index 0000000000..339bc32ee5
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/direct-from-seller-signals.https.window.js
@@ -0,0 +1,616 @@
+// META: script=/resources/testdriver.js
+// META: script=/common/utils.js
+// META: script=resources/fledge-util.sub.js
+// META: script=/common/subset-tests.js
+// META: timeout=long
+// META: variant=?1-4
+// META: variant=?5-8
+// META: variant=?9-12
+// META: variant=?13-16
+// META: variant=?17-20
+// META: variant=?21-24
+// META: variant=?25-28
+// META: variant=?29-last
+
+"use strict;"
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await fetchDirectFromSellerSignals({ 'Buyer-Origin': window.location.origin });
+ await runReportTest(
+ test, uuid,
+ directFromSellerSignalsValidatorCode(
+ uuid, /*expectedSellerSignals=*/ null,
+ /*expectedAuctionSignals=*/ null, /*expectedPerBuyerSignals=*/ null),
+ // expectedReportUrls
+ [createSellerReportURL(uuid), createBidderReportURL(uuid)],
+ // renderURLOverride
+ null,
+ // auctionConfigOverrides
+ {directFromSellerSignalsHeaderAdSlot: 'adSlot/0'});
+}, 'Test directFromSellerSignals with empty Ad-Auction-Signals header.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await fetchDirectFromSellerSignals({ 'Buyer-Origin': window.location.origin });
+ await runReportTest(
+ test, uuid,
+ directFromSellerSignalsValidatorCode(
+ uuid, 'sellerSignals/1',
+ /*expectedAuctionSignals=*/null, /*expectedPerBuyerSignals=*/null),
+ // expectedReportUrls
+ [createSellerReportURL(uuid), createBidderReportURL(uuid)],
+ // renderURLOverride
+ null,
+ // auctionConfigOverrides
+ { directFromSellerSignalsHeaderAdSlot: 'adSlot/1' }
+ );
+}, 'Test directFromSellerSignals with only sellerSignals.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await fetchDirectFromSellerSignals({ 'Buyer-Origin': window.location.origin });
+ await runReportTest(
+ test, uuid,
+ directFromSellerSignalsValidatorCode(
+ uuid, /*expectedSellerSignals=*/null,
+ 'auctionSignals/2', /*expectedPerBuyerSignals=*/null),
+ // expectedReportUrls
+ [createSellerReportURL(uuid), createBidderReportURL(uuid)],
+ // renderURLOverride
+ null,
+ // auctionConfigOverrides
+ { directFromSellerSignalsHeaderAdSlot: 'adSlot/2' }
+ );
+}, 'Test directFromSellerSignals with only auctionSignals.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await fetchDirectFromSellerSignals({ 'Buyer-Origin': window.location.origin });
+ await runReportTest(
+ test, uuid,
+ directFromSellerSignalsValidatorCode(
+ uuid, /*expectedSellerSignals=*/null,
+ /*expectedAuctionSignals=*/null, 'perBuyerSignals/3'),
+ // expectedReportUrls
+ [createSellerReportURL(uuid), createBidderReportURL(uuid)],
+ // renderURLOverride
+ null,
+ // auctionConfigOverrides
+ { directFromSellerSignalsHeaderAdSlot: 'adSlot/3' }
+ );
+}, 'Test directFromSellerSignals with only perBuyerSignals.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await fetchDirectFromSellerSignals({ 'Buyer-Origin': window.location.origin });
+ await runReportTest(
+ test, uuid,
+ directFromSellerSignalsValidatorCode(
+ uuid, 'sellerSignals/4',
+ 'auctionSignals/4', 'perBuyerSignals/4'),
+ // expectedReportUrls
+ [createSellerReportURL(uuid), createBidderReportURL(uuid)],
+ // renderURLOverride
+ null,
+ // auctionConfigOverrides
+ { directFromSellerSignalsHeaderAdSlot: 'adSlot/4' }
+ );
+}, 'Test directFromSellerSignals with sellerSignals, auctionSignals and perBuyerSignals.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await fetchDirectFromSellerSignals({ 'Buyer-Origin': window.location.origin });
+ await runReportTest(
+ test, uuid,
+ directFromSellerSignalsValidatorCode(
+ uuid, 'sellerSignals/1',
+ /*expectedAuctionSignals=*/null, /*expectedPerBuyerSignals=*/null),
+ // expectedReportUrls
+ [createSellerReportURL(uuid), createBidderReportURL(uuid)],
+ // renderURLOverride
+ null,
+ // auctionConfigOverrides
+ { directFromSellerSignalsHeaderAdSlot: 'adSlot/1' }
+ );
+
+ await runReportTest(
+ test, uuid,
+ directFromSellerSignalsValidatorCode(
+ uuid, /*expectedSellerSignals=*/null,
+ 'auctionSignals/2', /*expectedPerBuyerSignals=*/null),
+ // expectedReportUrls
+ [createSellerReportURL(uuid), createBidderReportURL(uuid)],
+ // renderURLOverride
+ null,
+ // auctionConfigOverrides
+ { directFromSellerSignalsHeaderAdSlot: 'adSlot/2' }
+ );
+}, 'Test directFromSellerSignals with single fetch and multiple auctions');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ const ad_slot = Promise.resolve('adSlot/4');
+ await fetchDirectFromSellerSignals({ 'Buyer-Origin': window.location.origin });
+ await runReportTest(
+ test, uuid,
+ directFromSellerSignalsValidatorCode(
+ uuid, 'sellerSignals/4',
+ 'auctionSignals/4', 'perBuyerSignals/4'),
+ // expectedReportUrls
+ [createSellerReportURL(uuid), createBidderReportURL(uuid)],
+ // renderURLOverride
+ null,
+ // auctionConfigOverrides
+ { directFromSellerSignalsHeaderAdSlot: ad_slot }
+ );
+}, 'Test directFromSellerSignals with resolved promise ad slot.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await fetchDirectFromSellerSignals({ 'Buyer-Origin': window.location.origin });
+ await joinInterestGroup(test, uuid);
+
+ const adSlot = Promise.reject(new Error('This is a rejected promise.'));
+ let auctionConfig =
+ { seller: window.location.origin,
+ interestGroupBuyers: [window.location.origin],
+ resolveToConfig: true,
+ decisionLogicURL: createDecisionScriptURL(uuid),
+ directFromSellerSignalsHeaderAdSlot: adSlot };
+
+ try {
+ await navigator.runAdAuction(auctionConfig);
+ } catch(e) {
+ assert_true(e instanceof TypeError);
+ return;
+ }
+ throw "Exception unexpectedly not thrown.";
+}, 'Test directFromSellerSignals with rejected promise ad slot.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ const validator = directFromSellerSignalsValidatorCode(
+ uuid, 'sellerSignals/4',
+ 'auctionSignals/4', 'perBuyerSignals/4');
+ let reportResult = `if (!(${validator.reportResultSuccessCondition})) {
+ sendReportTo('${createSellerReportURL(uuid, 'error')}');
+ return false;
+ }
+ ${validator.reportResult}`;
+ let reportWin = `if (!(${validator.reportWinSuccessCondition})) {
+ sendReportTo('${createBidderReportURL(uuid, 'error')}');
+ return false;
+ }
+ ${validator.reportWin}`;
+ let decisionScriptURLParams = { scoreAd : validator.scoreAd,
+ reportResult : reportResult };
+ let biddingScriptURLParams = { generateBid : validator.generateBid,
+ reportWin : reportWin };
+ let interestGroupOverrides =
+ { biddingLogicURL: createBiddingScriptURL(biddingScriptURLParams) };
+ await joinInterestGroup(test, uuid, interestGroupOverrides);
+
+ let adSlotResolve = null;
+ const adSlotPromise = new Promise((resolve, reject) => { adSlotResolve = resolve });
+ let auctionConfig =
+ { seller: window.location.origin,
+ interestGroupBuyers: [window.location.origin],
+ resolveToConfig: true,
+ decisionLogicURL: createDecisionScriptURL(uuid, decisionScriptURLParams),
+ directFromSellerSignalsHeaderAdSlot: adSlotPromise };
+ let resultPromise = navigator.runAdAuction(auctionConfig);
+
+ await fetchDirectFromSellerSignals({ 'Buyer-Origin': window.location.origin });
+ adSlotResolve('adSlot/4');
+ let result = await resultPromise;
+ createAndNavigateFencedFrame(test, result);
+ await waitForObservedRequests(uuid, [createSellerReportURL(uuid), createBidderReportURL(uuid)]);
+}, 'Test directFromSellerSignals that runAdAuction will wait until the promise of fetch is resolved.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await fetchDirectFromSellerSignals({ 'Buyer-Origin': window.location.origin });
+ await runReportTest(
+ test, uuid,
+ directFromSellerSignalsValidatorCode(
+ uuid, 'sellerSignals/5',
+ 'auctionSignals/5', /*expectedPerBuyerSignals=*/null),
+ // expectedReportUrls
+ [createSellerReportURL(uuid), createBidderReportURL(uuid)],
+ // renderURLOverride
+ null,
+ // auctionConfigOverrides
+ { directFromSellerSignalsHeaderAdSlot: 'adSlot/5' }
+ );
+}, 'Test directFromSellerSignals with mismatched perBuyerSignals.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await fetchDirectFromSellerSignals({ 'Buyer-Origin': '*' });
+ await runReportTest(
+ test, uuid,
+ directFromSellerSignalsValidatorCode(
+ uuid, 'sellerSignals/5',
+ 'auctionSignals/5', /*expectedPerBuyerSignals=*/null),
+ // expectedReportUrls
+ [createSellerReportURL(uuid), createBidderReportURL(uuid)],
+ // renderURLOverride
+ null,
+ // auctionConfigOverrides
+ { directFromSellerSignalsHeaderAdSlot: 'adSlot/5' }
+ );
+}, 'Test directFromSellerSignals does not support wildcard for buyerOrigin of perBuyerSignals.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await fetchDirectFromSellerSignals({ 'Buyer-Origin': window.location.origin });
+ await runReportTest(
+ test, uuid,
+ directFromSellerSignalsValidatorCode(
+ uuid, /*expectedSellerSignals=*/null,
+ /*expectedAuctionSignals=*/null, /*expectedPerBuyerSignals=*/null),
+ // expectedReportUrls
+ [createSellerReportURL(uuid), createBidderReportURL(uuid)],
+ // renderURLOverride
+ null,
+ // auctionConfigOverrides
+ { directFromSellerSignalsHeaderAdSlot: 'adSlot/non-exist' }
+ );
+}, 'Test directFromSellerSignals with non-existent adSlot.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await fetchDirectFromSellerSignals({ 'Buyer-Origin': window.location.origin });
+ await runReportTest(
+ test, uuid,
+ directFromSellerSignalsValidatorCode(
+ uuid, /*expectedSellerSignals=*/null,
+ /*expectedAuctionSignals=*/null, /*expectedPerBuyerSignals=*/null),
+ [createSellerReportURL(uuid), createBidderReportURL(uuid)],
+ // renderURLOverride
+ null,
+ // auctionConfigOverrides
+ { directFromSellerSignalsHeaderAdSlot: null }
+ );
+}, 'Test directFromSellerSignals with null directFromSellerSignalsHeaderAdSlot.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await fetchDirectFromSellerSignals({ 'Buyer-Origin': window.location.origin });
+ await runReportTest(
+ test, uuid,
+ directFromSellerSignalsValidatorCode(
+ uuid, /*expectedSellerSignals=*/null,
+ /*expectedAuctionSignals=*/null, /*expectedPerBuyerSignals=*/null),
+ [createSellerReportURL(uuid), createBidderReportURL(uuid)]
+ );
+}, 'Test directFromSellerSignals with no directFromSellerSignalsHeaderAdSlot.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await fetchDirectFromSellerSignals({ 'Negative-Test-Option': 'HTTP Error' });
+ await runReportTest(
+ test, uuid,
+ directFromSellerSignalsValidatorCode(
+ uuid, /*expectedSellerSignals=*/null,
+ /*expectedAuctionSignals=*/null, /*expectedPerBuyerSignals=*/null),
+ // expectedReportUrls
+ [createSellerReportURL(uuid), createBidderReportURL(uuid)],
+ // renderURLOverride
+ null,
+ // auctionConfigOverrides
+ { directFromSellerSignalsHeaderAdSlot: 'adSlot' }
+ );
+}, 'Test directFromSellerSignals with HTTP error.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await fetchDirectFromSellerSignals({ 'Negative-Test-Option': 'No Ad-Auction-Signals Header' });
+ await runReportTest(
+ test, uuid,
+ directFromSellerSignalsValidatorCode(
+ uuid, /*expectedSellerSignals=*/null,
+ /*expectedAuctionSignals=*/null, /*expectedPerBuyerSignals=*/null),
+ // expectedReportUrls
+ [createSellerReportURL(uuid), createBidderReportURL(uuid)],
+ // renderURLOverride
+ null,
+ // auctionConfigOverrides
+ { directFromSellerSignalsHeaderAdSlot: 'adSlot' }
+ );
+}, 'Test directFromSellerSignals with no returned Ad-Auction-Signals Header.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await fetchDirectFromSellerSignals({ 'Negative-Test-Option': 'Invalid Json' });
+ await runReportTest(
+ test, uuid,
+ directFromSellerSignalsValidatorCode(
+ uuid, /*expectedSellerSignals=*/null,
+ /*expectedAuctionSignals=*/null, /*expectedPerBuyerSignals=*/null),
+ // expectedReportUrls
+ [createSellerReportURL(uuid), createBidderReportURL(uuid)],
+ // renderURLOverride
+ null,
+ // auctionConfigOverrides
+ { directFromSellerSignalsHeaderAdSlot: 'adSlot' }
+ );
+}, 'Test directFromSellerSignals with invalid json in Ad-Auction-Signals header.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ let codeToInsert = directFromSellerSignalsValidatorCode(
+ uuid, /*expectedSellerSignals=*/null,
+ /*expectedAuctionSignals=*/null, /*expectedPerBuyerSignals=*/null);
+ codeToInsert.decisionScriptURLOrigin = OTHER_ORIGIN1;
+ await fetchDirectFromSellerSignals({ 'Buyer-Origin': window.location.origin });
+ await runReportTest(
+ test, uuid, codeToInsert,
+ // expectedReportUrls
+ [createSellerReportURL(uuid), createBidderReportURL(uuid)],
+ // renderURLOverride
+ null,
+ // auctionConfigOverrides
+ { directFromSellerSignalsHeaderAdSlot: 'adSlot/4',
+ seller: OTHER_ORIGIN1 }
+ );
+}, 'Test directFromSellerSignals with different fetch and seller origins.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ let codeToInsert = directFromSellerSignalsValidatorCode(
+ uuid, 'sellerSignals/4',
+ 'auctionSignals/4', 'perBuyerSignals/4');
+ codeToInsert.decisionScriptURLOrigin = OTHER_ORIGIN1;
+ await fetchDirectFromSellerSignals({ 'Buyer-Origin': window.location.origin }, OTHER_ORIGIN1);
+ await runReportTest(
+ test, uuid, codeToInsert,
+ // expectedReportUrls
+ [createSellerReportURL(uuid), createBidderReportURL(uuid)],
+ // renderURLOverride
+ null,
+ // auctionConfigOverrides
+ { directFromSellerSignalsHeaderAdSlot: 'adSlot/4',
+ seller: OTHER_ORIGIN1 }
+ );
+}, 'Test directFromSellerSignals with same fetch and seller origins.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ let iframe = await createIframe(test, OTHER_ORIGIN1);
+ await runInFrame(test, iframe, `await joinInterestGroup(test_instance, "${uuid}");`);
+ await fetchDirectFromSellerSignals({ 'Buyer-Origin': OTHER_ORIGIN1 });
+ await runReportTest(
+ test, uuid,
+ directFromSellerSignalsValidatorCode(
+ uuid, 'sellerSignals/4',
+ 'auctionSignals/4', 'perBuyerSignals/4'),
+ // expectedReportUrls
+ [createSellerReportURL(uuid), createBidderReportURL(uuid, '1', OTHER_ORIGIN1)],
+ // renderURLOverride
+ null,
+ // auctionConfigOverrides
+ { directFromSellerSignalsHeaderAdSlot: 'adSlot/4',
+ interestGroupBuyers: [OTHER_ORIGIN1] }
+ );
+}, 'Test directFromSellerSignals different interest group owner origin from top frame.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ let iframe = await createIframe(test, OTHER_ORIGIN1, "join-ad-interest-group; run-ad-auction");
+ await fetchDirectFromSellerSignals({ 'Buyer-Origin': OTHER_ORIGIN1 }, OTHER_ORIGIN1);
+ await runInFrame(
+ test, iframe,
+ `await runReportTest(
+ test_instance, "${uuid}",
+ directFromSellerSignalsValidatorCode(
+ "${uuid}", 'sellerSignals/4', 'auctionSignals/4', 'perBuyerSignals/4'),
+ // expectedReportUrls
+ [createSellerReportURL("${uuid}"), createBidderReportURL("${uuid}")],
+ // renderURLOverride
+ null,
+ // auctionConfigOverrides
+ { directFromSellerSignalsHeaderAdSlot: 'adSlot/4' })`);
+}, 'Test directFromSellerSignals with fetching in top frame and running auction in iframe.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ let iframe = await createIframe(test, OTHER_ORIGIN1, "join-ad-interest-group; run-ad-auction");
+ await runInFrame(
+ test, iframe,
+ `await fetchDirectFromSellerSignals({ 'Buyer-Origin': window.location.origin });
+ await runReportTest(
+ test_instance, "${uuid}",
+ directFromSellerSignalsValidatorCode(
+ "${uuid}", 'sellerSignals/4',
+ 'auctionSignals/4', 'perBuyerSignals/4'),
+ // expectedReportUrls
+ [createSellerReportURL("${uuid}"), createBidderReportURL("${uuid}")],
+ // renderURLOverride
+ null,
+ // auctionConfigOverrides
+ { directFromSellerSignalsHeaderAdSlot: 'adSlot/4' })`);
+}, 'Test directFromSellerSignals with fetching and running auction in the same iframe.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ let iframe1 = await createIframe(test, OTHER_ORIGIN1);
+ let iframe2 = await createIframe(test, OTHER_ORIGIN2, "join-ad-interest-group; run-ad-auction");
+ await runInFrame(
+ test, iframe1,
+ `await fetchDirectFromSellerSignals({ 'Buyer-Origin': OTHER_ORIGIN2 }, OTHER_ORIGIN2);`);
+ await runInFrame(
+ test, iframe2,
+ `await runReportTest(
+ test_instance, "${uuid}",
+ directFromSellerSignalsValidatorCode(
+ "${uuid}", 'sellerSignals/4',
+ 'auctionSignals/4', 'perBuyerSignals/4'),
+ // expectedReportUrls
+ [createSellerReportURL("${uuid}"), createBidderReportURL("${uuid}")],
+ // renderURLOverride
+ null,
+ // auctionConfigOverrides
+ { directFromSellerSignalsHeaderAdSlot: 'adSlot/4' })`);
+}, 'Test directFromSellerSignals with fetching in iframe 1 and running auction in iframe 2.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ let iframe = await createIframe(test, OTHER_ORIGIN1);
+ await runInFrame(
+ test, iframe,
+ `await fetchDirectFromSellerSignals(
+ { 'Buyer-Origin': "${window.location.origin}" }, "${window.location.origin}");`);
+ await runReportTest(
+ test, uuid,
+ directFromSellerSignalsValidatorCode(
+ uuid, 'sellerSignals/4',
+ 'auctionSignals/4', 'perBuyerSignals/4'),
+ // expectedReportUrls
+ [createSellerReportURL(uuid), createBidderReportURL(uuid)],
+ // renderURLOverride
+ null,
+ // auctionConfigOverrides
+ { directFromSellerSignalsHeaderAdSlot: 'adSlot/4'}
+ );
+}, 'Test directFromSellerSignals with fetching in iframe and running auction in top frame.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await fetchDirectFromSellerSignals({ 'Negative-Test-Option': 'Network Error' });
+ await runReportTest(
+ test, uuid,
+ directFromSellerSignalsValidatorCode(
+ uuid, 'sellerSignals',
+ 'auctionSignals', /*expectedPerBuyerSignals=*/null),
+ // expectedReportUrls
+ [createSellerReportURL(uuid), createBidderReportURL(uuid)],
+ // renderURLOverride
+ null,
+ // auctionConfigOverrides
+ { directFromSellerSignalsHeaderAdSlot: 'adSlot' }
+ );
+}, 'Test directFromSellerSignals with network error.');
+
+subsetTest(promise_test, async test => {
+ let dfss = false;
+ navigator.runAdAuction({
+ get directFromSellerSignalsHeaderAdSlot() { dfss = true; }
+ }).catch((e) => {});
+ assert_true(dfss);
+}, 'Test directFromSellerSignals feature detection.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await fetchDirectFromSellerSignals({ 'Buyer-Origin': window.location.origin });
+ await fetchDirectFromSellerSignals(
+ { 'Buyer-Origin': window.location.origin, 'Alternative-Response': 'Overwrite adSlot/1'});
+ await runReportTest(
+ test, uuid,
+ directFromSellerSignalsValidatorCode(
+ uuid, 'altSellerSignals/1',
+ /*expectedAuctionSignals=*/null, /*expectedPerBuyerSignals=*/null),
+ // expectedReportUrls
+ [createSellerReportURL(uuid), createBidderReportURL(uuid)],
+ // renderURLOverride
+ null,
+ // auctionConfigOverrides
+ { directFromSellerSignalsHeaderAdSlot: 'adSlot/1' }
+ );
+}, 'Test directFromSellerSignals with 2 responses -- the later overwrites the former.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await fetchDirectFromSellerSignals({ 'Buyer-Origin': window.location.origin });
+ await fetchDirectFromSellerSignals(
+ { 'Buyer-Origin': window.location.origin, 'Alternative-Response': 'Overwrite adSlot/1'});
+ await fetchDirectFromSellerSignals(
+ { 'Buyer-Origin': window.location.origin, 'Alternative-Response': 'Overwrite adSlot/1 v2'});
+ await runReportTest(
+ test, uuid,
+ directFromSellerSignalsValidatorCode(
+ uuid, 'altV2SellerSignals/1',
+ /*expectedAuctionSignals=*/null, /*expectedPerBuyerSignals=*/null),
+ // expectedReportUrls
+ [createSellerReportURL(uuid), createBidderReportURL(uuid)],
+ // renderURLOverride
+ null,
+ // auctionConfigOverrides
+ { directFromSellerSignalsHeaderAdSlot: 'adSlot/1' }
+ );
+}, 'Test directFromSellerSignals with 3 responses -- the last response overwrites the former responses.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await fetchDirectFromSellerSignals({ 'Buyer-Origin': window.location.origin });
+ await fetchDirectFromSellerSignals(
+ { 'Buyer-Origin': window.location.origin, 'Alternative-Response': 'Overwrite adSlot/1'});
+ await runReportTest(
+ test, uuid,
+ directFromSellerSignalsValidatorCode(
+ uuid, /*expectedSellerSignals=*/null,
+ 'auctionSignals/2', /*expectedPerBuyerSignals=*/null),
+ // expectedReportUrls
+ [createSellerReportURL(uuid), createBidderReportURL(uuid)],
+ // renderURLOverride
+ null,
+ // auctionConfigOverrides
+ { directFromSellerSignalsHeaderAdSlot: 'adSlot/2' }
+ );
+}, 'Test directFromSellerSignals with 2 responses -- old non-overwritten ad slot remains.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await fetchDirectFromSellerSignals(
+ { 'Buyer-Origin': window.location.origin, 'Alternative-Response': 'Duplicate adSlot/1'});
+ await runReportTest(
+ test, uuid,
+ directFromSellerSignalsValidatorCode(
+ uuid, 'firstSellerSignals/1',
+ /*expectedAuctionSignals=*/null, /*expectedPerBuyerSignals=*/null),
+ // expectedReportUrls
+ [createSellerReportURL(uuid), createBidderReportURL(uuid)],
+ // renderURLOverride
+ null,
+ // auctionConfigOverrides
+ { directFromSellerSignalsHeaderAdSlot: 'adSlot/1' }
+ );
+}, 'Test invalid directFromSellerSignals with duplicate adSlot in response -- the second is ignored.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await fetchDirectFromSellerSignals(
+ { 'Buyer-Origin': window.location.origin, 'Alternative-Response': 'Duplicate adSlot/1'});
+ await runReportTest(
+ test, uuid,
+ directFromSellerSignalsValidatorCode(
+ uuid, 'nonDupSellerSignals/2',
+ /*expectedAuctionSignals=*/null, /*expectedPerBuyerSignals=*/null),
+ // expectedReportUrls
+ [createSellerReportURL(uuid), createBidderReportURL(uuid)],
+ // renderURLOverride
+ null,
+ // auctionConfigOverrides
+ { directFromSellerSignalsHeaderAdSlot: 'adSlot/2' }
+ );
+}, 'Test invalid directFromSellerSignals with duplicate adSlot in response, selecting a non duplicated adSlot.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await fetchDirectFromSellerSignals(
+ { 'Buyer-Origin': window.location.origin,
+ 'Alternative-Response': 'Two keys with same values'});
+ await runReportTest(
+ test, uuid,
+ directFromSellerSignalsValidatorCode(
+ uuid, 'sameSellerSignals',
+ 'sameAuctionSignals', 'samePerBuyerSignals'),
+ // expectedReportUrls
+ [createSellerReportURL(uuid), createBidderReportURL(uuid)],
+ // renderURLOverride
+ null,
+ // auctionConfigOverrides
+ { directFromSellerSignalsHeaderAdSlot: 'adSlot/1' }
+ );
+}, 'Test invalid directFromSellerSignals with duplicate values in response.');
diff --git a/testing/web-platform/tests/fledge/tentative/fetch-ad-auction-headers-insecure-context.tentative.http.html b/testing/web-platform/tests/fledge/tentative/fetch-ad-auction-headers-insecure-context.tentative.http.html
new file mode 100644
index 0000000000..d3bdb80175
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/fetch-ad-auction-headers-insecure-context.tentative.http.html
@@ -0,0 +1,11 @@
+<!doctype html>
+<body>
+ <script src=/resources/testharness.js></script>
+ <script src=/resources/testharnessreport.js></script>
+ <script>
+ promise_test(async t => {
+ return promise_rejects_dom(t, 'NotAllowedError',
+ fetch('./resources/empty.html', {adAuctionHeaders: true}));
+ }, 'test fetch(<url>, {adAuctionHeaders: true}) in insecure context');
+ </script>
+</body>
diff --git a/testing/web-platform/tests/fledge/tentative/fetch-ad-auction-headers.tentative.https.html b/testing/web-platform/tests/fledge/tentative/fetch-ad-auction-headers.tentative.https.html
new file mode 100644
index 0000000000..7b2a2c2ba4
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/fetch-ad-auction-headers.tentative.https.html
@@ -0,0 +1,12 @@
+<!doctype html>
+<body>
+ <script src=/resources/testharness.js></script>
+ <script src=/resources/testharnessreport.js></script>
+ <script>
+ promise_test(async t => {
+ let response = await fetch('./resources/empty.html', {adAuctionHeaders: true});
+ // TODO(crbug.com/1442274): Check the request header when the functions
+ // are fully implemented.
+ }, 'test fetch(<url>, {adAuctionHeaders: true})');
+ </script>
+</body>
diff --git a/testing/web-platform/tests/fledge/tentative/generate-bid-recency.https.window.js b/testing/web-platform/tests/fledge/tentative/generate-bid-recency.https.window.js
new file mode 100644
index 0000000000..07da463a2d
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/generate-bid-recency.https.window.js
@@ -0,0 +1,34 @@
+// META: script=/resources/testdriver.js
+// META: script=/common/utils.js
+// META: script=resources/fledge-util.sub.js
+// META: timeout=long
+
+"use strict;"
+
+promise_test(async test => {
+ const uuid = generateUuid(test);
+ await runReportTest(
+ test, uuid,
+ { generateBid:
+ `if (browserSignals.recency === undefined)
+ throw new Error("Missing recency in browserSignals.")
+
+ if (browserSignals.recency < 0)
+ throw new Error("Recency is a negative value.")
+
+ if (browserSignals.recency > 30000)
+ throw new Error("Recency is over 30 seconds threshold.")
+
+ if (browserSignals.recency % 100 !== 0)
+ throw new Error("Recency is not rounded to multiple of 100 milliseconds.")
+
+ return {'bid': 9,
+ 'render': interestGroup.ads[0].renderURL};`,
+ reportWin:
+ `sendReportTo('${createBidderReportURL(uuid)}');`
+ },
+ // expectedReportUrls
+ [createBidderReportURL(uuid)]
+ );
+}, 'Check recency in generateBid() is below a certain threshold and rounded ' +
+ 'to multiple of 100 milliseconds.');
diff --git a/testing/web-platform/tests/fledge/tentative/insecure-context.window.js b/testing/web-platform/tests/fledge/tentative/insecure-context.window.js
new file mode 100644
index 0000000000..9016277b73
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/insecure-context.window.js
@@ -0,0 +1,8 @@
+"use strict";
+
+test(() => {
+ assert_false('joinAdInterestGroup' in navigator, 'joinAdInterestGroup not available.');
+ assert_false('leaveAdInterestGroup' in navigator, 'leaveAdInterestGroup not available.');
+ assert_false('runAdAuction' in navigator, 'runAdAuction not available.');
+ assert_false('updateAdInterestGroups' in navigator, 'updateAdInterestGroups not available.');
+}, "Fledge requires secure context.");
diff --git a/testing/web-platform/tests/fledge/tentative/interest-group-passed-to-generate-bid.https.window.js b/testing/web-platform/tests/fledge/tentative/interest-group-passed-to-generate-bid.https.window.js
new file mode 100644
index 0000000000..2fb346bbe3
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/interest-group-passed-to-generate-bid.https.window.js
@@ -0,0 +1,737 @@
+// META: script=/resources/testdriver.js
+// META: script=/common/utils.js
+// META: script=resources/fledge-util.sub.js
+// META: script=/common/subset-tests.js
+// META: timeout=long
+// META: variant=?1-5
+// META: variant=?6-10
+// META: variant=?11-15
+// META: variant=?16-20
+// META: variant=?21-25
+// META: variant=?26-30
+// META: variant=?31-35
+// META: variant=?36-40
+// META: variant=?41-45
+// META: variant=?46-50
+// META: variant=?51-55
+// META: variant=?56-60
+// META: variant=?61-65
+// META: variant=?66-70
+// META: variant=?71-75
+// META: variant=?76-80
+// META: variant=?81-85
+
+"use strict;"
+
+// These tests focus on making sure InterestGroup fields are passed to generateBid(),
+// and are normalized if necessary. This test does not check the behaviors of the
+// fields.
+
+// Modifies "ads". Replaces "REPLACE_WITH_UUID" in all "renderURL" fields of
+// objects in "ads" array with "uuid". Generated ad URLs have embedded
+// UUIDs to prevent InterestGroups unexpectedly left over from one test from
+// messing up another test, but these tests need ad URLs before the UUID is
+// generated. To get around that, "REPLACE_WITH_UUID" is used in place of UUIDs
+// and then this is used to replace them with the real UUID.
+function updateAdRenderURLs(ads, uuid) {
+ for (let i = 0; i < ads.length; ++i) {
+ let ad = ads[i];
+ ad.renderURL = ad.renderURL.replace('REPLACE_WITH_UUID', uuid);
+ }
+}
+
+const makeTest = ({
+ // Test name.
+ name,
+ // InterestGroup field name.
+ fieldName,
+ // InterestGroup field value, both expected in worklets and the value used
+ // when joining the interest group. If undefined, value will not be set in
+ // interestGroup, and will be expected to also not be set in the
+ // interestGroup passed to generateBid().
+ fieldValue,
+ // Additional values to use in the InterestGroup passed to joinInterestGroup().
+ // If it contains a value for the key specified in `fieldName`, takes
+ // precedent over `fieldValue`.
+ interestGroupOverrides = {}
+}) => {
+ subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ // It's not strictly necessary to replace UUIDs in "adComponents", but do it for consistency.
+ if (fieldName === 'ads' || fieldName === 'adComponents' && fieldValue) {
+ updateAdRenderURLs(fieldValue, uuid);
+ }
+
+ if (interestGroupOverrides.ads) {
+ updateAdRenderURLs(interestGroupOverrides.ads, uuid);
+ }
+
+ if (interestGroupOverrides.adComponents) {
+ updateAdRenderURLs(interestGroupOverrides.adComponents, uuid);
+ }
+
+ if (!(fieldName in interestGroupOverrides) && fieldValue !== undefined)
+ interestGroupOverrides[fieldName] = fieldValue;
+
+ let comparison = `deepEquals(interestGroup["${fieldName}"], ${JSON.stringify(fieldValue)})`;
+ // In the case it's undefined, require value not to be set.
+ if (fieldValue === undefined)
+ comparison = `!("${fieldName}" in interestGroup)`;
+
+ // Prefer to use `interestGroupOverrides.owner` if present. Treat it as a URL
+ // and then convert it to an origin because one test passes in a URL.
+ let origin = location.origin;
+ if (interestGroupOverrides.owner)
+ origin = new URL(interestGroupOverrides.owner).origin;
+
+ interestGroupOverrides.biddingLogicURL =
+ createBiddingScriptURL(
+ { origin: origin,
+ generateBid:
+ `// Delete deprecated "renderUrl" fields from ads and adComponents, if
+ // present.
+ for (let field in interestGroup) {
+ if (field === "ads" || field === "adComponents") {
+ for (let i = 0; i < interestGroup[field].length; ++i) {
+ let ad = interestGroup[field][i];
+ delete ad.renderUrl;
+ }
+ }
+ }
+ if (!${comparison})
+ throw "Unexpected value: " + JSON.stringify(interestGroup["${fieldName}"]);`
+ });
+ if (origin !== location.origin) {
+ await joinCrossOriginInterestGroup(test, uuid, origin, interestGroupOverrides);
+ } else {
+ await joinInterestGroup(test, uuid, interestGroupOverrides);
+ }
+
+ await runBasicFledgeTestExpectingWinner(test, uuid, {interestGroupBuyers: [origin]});
+ }, name);
+};
+
+makeTest({
+ name: 'InterestGroup.owner.',
+ fieldName: 'owner',
+ fieldValue: OTHER_ORIGIN1
+});
+
+makeTest({
+ name: 'InterestGroup.owner with non-normalized origin.',
+ fieldName: 'owner',
+ fieldValue: OTHER_ORIGIN1,
+ interestGroupOverrides: {owner: ` ${OTHER_ORIGIN1.toUpperCase()} `}
+});
+
+makeTest({
+ name: 'InterestGroup.owner is URL.',
+ fieldName: 'owner',
+ fieldValue: OTHER_ORIGIN1,
+ interestGroupOverrides: {owner: OTHER_ORIGIN1 + '/Foopy'}
+});
+
+makeTest({
+ name: 'InterestGroup.name.',
+ fieldName: 'name',
+ fieldValue: 'Jim'
+});
+
+makeTest({
+ name: 'InterestGroup.name with unicode characters.',
+ fieldName: 'name',
+ fieldValue: '\u2665'
+});
+
+makeTest({
+ name: 'InterestGroup.name with empty name.',
+ fieldName: 'name',
+ fieldValue: ''
+});
+
+makeTest({
+ name: 'InterestGroup.name with unpaired surrogate characters, which should be replaced with "\\uFFFD".',
+ fieldName: 'name',
+ fieldValue: '\uFFFD,\uFFFD',
+ interestGroupOverrides: {name: '\uD800,\uDBF0'}
+});
+
+// Since "biddingLogicURL" contains the script itself inline, can't include the entire URL
+// in the script for an equality check. Instead, replace the "generateBid" query parameter
+// in the URL with an empty value before comparing it. This doesn't just delete the entire
+// query parameter to make sure that's correctly passed in.
+subsetTest(promise_test,async test => {
+ const uuid = generateUuid(test);
+
+ let biddingScriptBaseURL = createBiddingScriptURL({origin: OTHER_ORIGIN1, generateBid: ''});
+ let biddingLogicURL = createBiddingScriptURL(
+ { origin: OTHER_ORIGIN1,
+ generateBid:
+ `let biddingScriptBaseURL =
+ interestGroup.biddingLogicURL.replace(/generateBid=[^&]*/, "generateBid=");
+ if (biddingScriptBaseURL !== "${biddingScriptBaseURL}")
+ throw "Wrong bidding script URL: " + interestGroup.biddingLogicURL`
+ });
+
+ await joinCrossOriginInterestGroup(test, uuid, OTHER_ORIGIN1,
+ { biddingLogicURL: biddingLogicURL });
+
+ await runBasicFledgeTestExpectingWinner(test, uuid, {interestGroupBuyers: [OTHER_ORIGIN1]});
+}, 'InterestGroup.biddingLogicURL.');
+
+// Much like above test, but use a relative URL that points to bidding script.
+subsetTest(promise_test,async test => {
+ const uuid = generateUuid(test);
+
+ let biddingScriptBaseURL = createBiddingScriptURL({generateBid: ''});
+ let biddingLogicURL = createBiddingScriptURL(
+ { generateBid:
+ `let biddingScriptBaseURL =
+ interestGroup.biddingLogicURL.replace(/generateBid=[^&]*/, "generateBid=");
+ if (biddingScriptBaseURL !== "${biddingScriptBaseURL}")
+ throw "Wrong bidding script URL: " + interestGroup.biddingLogicURL`
+ });
+ biddingLogicURL = biddingLogicURL.replace(BASE_URL, 'foo/../');
+
+ await joinInterestGroup(test, uuid, { biddingLogicURL: biddingLogicURL });
+
+ await runBasicFledgeTestExpectingWinner(test, uuid);
+}, 'InterestGroup.biddingLogicURL with relative URL.');
+
+makeTest({
+ name: 'InterestGroup.lifetimeMs should not be passed in.',
+ fieldName: 'lifetimeMs',
+ fieldValue: undefined,
+ interestGroupOverrides: { lifetimeMs: "120000" }
+});
+
+makeTest({
+ name: 'InterestGroup.priority should not be passed in, since it can be changed by auctions.',
+ fieldName: 'priority',
+ fieldValue: undefined,
+ interestGroupOverrides: { priority: 500 }
+});
+
+makeTest({
+ name: 'InterestGroup.priorityVector undefined.',
+ fieldName: 'priorityVector',
+ fieldValue: undefined
+});
+
+makeTest({
+ name: 'InterestGroup.priorityVector empty.',
+ fieldName: 'priorityVector',
+ fieldValue: {}
+});
+
+makeTest({
+ name: 'InterestGroup.priorityVector.',
+ fieldName: 'priorityVector',
+ fieldValue: { 'a': -1, 'b': 2 }
+});
+
+// TODO: This is currently using USVString internally, so doesn't allow unpaired
+// surrogates, but the spec says it should.
+makeTest({
+ name: 'InterestGroup.priorityVector with unpaired surrogate character.',
+ fieldName: 'priorityVector',
+ fieldValue: { '\uFFFD': -1 },
+ interestGroupOverrides: { prioritySignalsOverrides: { '\uD800': -1 } }
+});
+
+makeTest({
+ name: 'InterestGroup.prioritySignalsOverrides should not be passed in, since it can be changed by auctions.',
+ fieldName: 'prioritySignalsOverrides',
+ fieldValue: undefined,
+ interestGroupOverrides: { prioritySignalsOverrides: { 'a': 1, 'b': 2 } }
+});
+
+makeTest({
+ name: 'InterestGroup.enableBiddingSignalsPrioritization not set.',
+ fieldName: 'enableBiddingSignalsPrioritization',
+ fieldValue: false,
+ interestGroupOverrides: { enableBiddingSignalsPrioritization: undefined }
+});
+
+makeTest({
+ name: 'InterestGroup.enableBiddingSignalsPrioritization unrecognized.',
+ fieldName: 'enableBiddingSignalsPrioritization',
+ // Non-empty strings are treated as true by Javascript. This test is serves
+ // to make sure that the 'foo' isn't preserved.
+ fieldValue: true,
+ interestGroupOverrides: { enableBiddingSignalsPrioritization: 'foo' }
+});
+
+makeTest({
+ name: 'InterestGroup.enableBiddingSignalsPrioritization false.',
+ fieldName: 'enableBiddingSignalsPrioritization',
+ fieldValue: false
+});
+
+makeTest({
+ name: 'InterestGroup.enableBiddingSignalsPrioritization true.',
+ fieldName: 'enableBiddingSignalsPrioritization',
+ fieldValue: true
+});
+
+makeTest({
+ name: 'InterestGroup.biddingWasmHelperURL not set.',
+ fieldName: 'biddingWasmHelperURL',
+ fieldValue: undefined
+});
+
+makeTest({
+ name: 'InterestGroup.biddingWasmHelperURL.',
+ fieldName: 'biddingWasmHelperURL',
+ fieldValue: `${OTHER_ORIGIN1}${RESOURCE_PATH}wasm-helper.py`,
+ interestGroupOverrides: {owner: OTHER_ORIGIN1}
+});
+
+makeTest({
+ name: 'InterestGroup.biddingWasmHelperURL with non-normalized value.',
+ fieldName: 'biddingWasmHelperURL',
+ fieldValue: `${OTHER_ORIGIN1}${RESOURCE_PATH}wasm-helper.py`,
+ interestGroupOverrides: {
+ owner: OTHER_ORIGIN1,
+ biddingWasmHelperURL:
+ `${OTHER_ORIGIN1.toUpperCase()}${RESOURCE_PATH}wasm-helper.py`
+ }
+});
+
+makeTest({
+ name: 'InterestGroup.biddingWasmHelperURL with relative URL.',
+ fieldName: 'biddingWasmHelperURL',
+ fieldValue: `${OTHER_ORIGIN1}${RESOURCE_PATH}wasm-helper.py`,
+ interestGroupOverrides: {
+ owner: OTHER_ORIGIN1,
+ biddingWasmHelperURL: 'foo/../resources/wasm-helper.py'
+ }
+});
+
+makeTest({
+ name: 'InterestGroup.biddingWasmHelperURL with unpaired surrogate characters, which should be replaced with "\\uFFFD".',
+ fieldName: 'biddingWasmHelperURL',
+ fieldValue: (new URL(`${OTHER_ORIGIN1}${RESOURCE_PATH}wasm-helper.py?\uFFFD.\uFFFD`)).href,
+ interestGroupOverrides: {
+ owner: OTHER_ORIGIN1,
+ biddingWasmHelperURL: `${OTHER_ORIGIN1}${RESOURCE_PATH}wasm-helper.py?\uD800.\uDBF0`
+ }
+});
+
+makeTest({
+ name: 'InterestGroup.updateURL not set.',
+ fieldName: 'updateURL',
+ fieldValue: undefined
+});
+
+makeTest({
+ name: 'InterestGroup.updateURL.',
+ fieldName: 'updateURL',
+ fieldValue: `${OTHER_ORIGIN1}${BASE_PATH}This-File-Does-Not-Exist.json`,
+ interestGroupOverrides: {owner: OTHER_ORIGIN1}
+});
+
+makeTest({
+ name: 'InterestGroup.updateURL with non-normalized value.',
+ fieldName: 'updateURL',
+ fieldValue: `${OTHER_ORIGIN1}${BASE_PATH}This-File-Does-Not-Exist.json`,
+ interestGroupOverrides: {
+ owner: OTHER_ORIGIN1,
+ updateURL: `${OTHER_ORIGIN1.toUpperCase()}${BASE_PATH}This-File-Does-Not-Exist.json`
+ }
+});
+
+makeTest({
+ name: 'InterestGroup.updateURL with relative URL.',
+ fieldName: 'updateURL',
+ fieldValue: (new URL(`${OTHER_ORIGIN1}${BASE_PATH}../This-File-Does-Not-Exist.json`)).href,
+ interestGroupOverrides: {
+ owner: OTHER_ORIGIN1,
+ updateURL: '../This-File-Does-Not-Exist.json'
+ }
+});
+
+makeTest({
+ name: 'InterestGroup.updateURL with unpaired surrogate characters, which should be replaced with "\\uFFFD".',
+ fieldName: 'updateURL',
+ fieldValue: (new URL(`${BASE_URL}\uFFFD.\uFFFD`)).href,
+ interestGroupOverrides: {
+ updateURL: `${BASE_URL}\uD800.\uDBF0`
+ }
+});
+
+makeTest({
+ name: 'InterestGroup.executionMode not present.',
+ fieldName: 'executionMode',
+ fieldValue: 'compatibility',
+ interestGroupOverrides: { executionMode: undefined }
+});
+
+makeTest({
+ name: 'InterestGroup.executionMode compatibility.',
+ fieldName: 'executionMode',
+ fieldValue: 'compatibility'
+});
+
+makeTest({
+ name: 'InterestGroup.executionMode frozen-context.',
+ fieldName: 'executionMode',
+ fieldValue: 'frozen-context'
+});
+
+makeTest({
+ name: 'InterestGroup.executionMode group-by-origin.',
+ fieldName: 'executionMode',
+ fieldValue: 'group-by-origin'
+});
+
+makeTest({
+ name: 'InterestGroup.executionMode has non-standard string.',
+ fieldName: 'executionMode',
+ fieldValue: 'compatibility',
+ interestGroupOverrides: { executionMode: 'foo' }
+});
+
+makeTest({
+ name: 'InterestGroup.trustedBiddingSignalsURL not set.',
+ fieldName: 'trustedBiddingSignalsURL',
+ fieldValue: undefined
+});
+
+makeTest({
+ name: 'InterestGroup.trustedBiddingSignalsURL.',
+ fieldName: 'trustedBiddingSignalsURL',
+ fieldValue: `${OTHER_ORIGIN1}${BASE_PATH}This-File-Does-Not-Exist.json`,
+ interestGroupOverrides: {owner: OTHER_ORIGIN1}
+});
+
+makeTest({
+ name: 'InterestGroup.trustedBiddingSignalsURL with non-normalized value.',
+ fieldName: 'trustedBiddingSignalsURL',
+ fieldValue: `${OTHER_ORIGIN1}${BASE_PATH}This-File-Does-Not-Exist.json`,
+ interestGroupOverrides: {
+ owner: OTHER_ORIGIN1,
+ trustedBiddingSignalsURL:
+ `${OTHER_ORIGIN1.toUpperCase()}${BASE_PATH}This-File-Does-Not-Exist.json`
+ }
+});
+
+makeTest({
+ name: 'InterestGroup.trustedBiddingSignalsURL with relative URL.',
+ fieldName: 'trustedBiddingSignalsURL',
+ fieldValue: (new URL(`${OTHER_ORIGIN1}${BASE_PATH}../This-File-Does-Not-Exist.json`)).href,
+ interestGroupOverrides: {
+ owner: OTHER_ORIGIN1,
+ trustedBiddingSignalsURL: '../This-File-Does-Not-Exist.json'
+ }
+});
+
+makeTest({
+ name: 'InterestGroup.trustedBiddingSignalsURL with unpaired surrogate characters, which should be replaced with "\\uFFFD".',
+ fieldName: 'trustedBiddingSignalsURL',
+ fieldValue: (new URL(`${BASE_URL}\uFFFD.\uFFFD`)).href,
+ interestGroupOverrides: {
+ trustedBiddingSignalsURL: `${BASE_URL}\uD800.\uDBF0`
+ }
+});
+
+makeTest({
+ name: 'InterestGroup.trustedBiddingSignalsKeys not set.',
+ fieldName: 'trustedBiddingSignalsKeys',
+ fieldValue: undefined
+});
+
+makeTest({
+ name: 'InterestGroup.trustedBiddingSignalsKeys.',
+ fieldName: 'trustedBiddingSignalsKeys',
+ fieldValue: ['a', ' b ', 'c', '1', '%20', '3', '\u2665']
+});
+
+makeTest({
+ name: 'InterestGroup.trustedBiddingSignalsKeys with non-normalized values.',
+ fieldName: 'trustedBiddingSignalsKeys',
+ fieldValue: ['1', '2', '3'],
+ interestGroupOverrides: { trustedBiddingSignalsKeys: [1, 0x2, '3'] }
+});
+
+makeTest({
+ name: 'InterestGroup.trustedBiddingSignalsKeys unpaired surrogate characters, which should be replaced with "\\uFFFD".',
+ fieldName: 'trustedBiddingSignalsKeys',
+ fieldValue: ['\uFFFD', '\uFFFD', '\uFFFD.\uFFFD'],
+ interestGroupOverrides: { trustedBiddingSignalsKeys: ['\uD800', '\uDBF0', '\uD800.\uDBF0'] }
+});
+
+makeTest({
+ name: 'InterestGroup.trustedBiddingSignalsSlotSizeMode empty.',
+ fieldName: 'trustedBiddingSignalsSlotSizeMode',
+ fieldValue: 'none',
+ interestGroupOverrides: { trustedBiddingSignalsSlotSizeMode: undefined }
+});
+
+makeTest({
+ name: 'InterestGroup.trustedBiddingSignalsSlotSizeMode none.',
+ fieldName: 'trustedBiddingSignalsSlotSizeMode',
+ fieldValue: 'none'
+});
+
+makeTest({
+ name: 'InterestGroup.trustedBiddingSignalsSlotSizeMode slot-size.',
+ fieldName: 'trustedBiddingSignalsSlotSizeMode',
+ fieldValue: 'slot-size'
+});
+
+makeTest({
+ name: 'InterestGroup.trustedBiddingSignalsSlotSizeMode all-slots-requested-sizes.',
+ fieldName: 'trustedBiddingSignalsSlotSizeMode',
+ fieldValue: 'all-slots-requested-sizes'
+});
+
+makeTest({
+ name: 'InterestGroup.trustedBiddingSignalsSlotSizeMode unrecognized value.',
+ fieldName: 'trustedBiddingSignalsSlotSizeMode',
+ fieldValue: 'none',
+ interestGroupOverrides: { trustedBiddingSignalsSlotSizeMode: 'unrecognized value' }
+});
+
+makeTest({
+ name: 'InterestGroup.userBiddingSignals not set.',
+ fieldName: 'userBiddingSignals',
+ fieldValue: undefined
+});
+
+makeTest({
+ name: 'InterestGroup.userBiddingSignals is integer.',
+ fieldName: 'userBiddingSignals',
+ fieldValue: 15
+});
+
+makeTest({
+ name: 'InterestGroup.userBiddingSignals is array.',
+ fieldName: 'userBiddingSignals',
+ fieldValue: [1, {a: 'b'}, 'c']
+});
+
+makeTest({
+ name: 'InterestGroup.userBiddingSignals is object.',
+ fieldName: 'userBiddingSignals',
+ fieldValue: {a:1, b:32.5, c:['d', 'e']}
+});
+
+makeTest({
+ name: 'InterestGroup.userBiddingSignals unpaired surrogate characters, which should be kept as-is.',
+ fieldName: 'userBiddingSignals',
+ fieldValue: '\uD800.\uDBF0'
+});
+
+makeTest({
+ name: 'InterestGroup.userBiddingSignals unpaired surrogate characters in an object, which should be kept as-is.',
+ fieldName: 'userBiddingSignals',
+ fieldValue: {'\uD800': '\uDBF0', '\uDBF0':['\uD800']}
+});
+
+makeTest({
+ name: 'InterestGroup.nonStandardField.',
+ fieldName: 'nonStandardField',
+ fieldValue: undefined,
+ interestGroupOverrides: {nonStandardField: 'This value should not be passed to worklets'}
+});
+
+// Note that all ad tests have a deprecated "renderUrl" field passed to generateBid.
+
+// Ad URLs need the right UUID for seller scripts to accept their bids. Since UUID changes
+// for each test, and is not available outside makeTest(), have to use string that will
+// be replaced with the real UUID.
+const AD1_URL = createRenderURL('REPLACE_WITH_UUID', /*script=*/';');
+const AD2_URL = createRenderURL('REPLACE_WITH_UUID', /*script=*/';;');
+
+makeTest({
+ name: 'InterestGroup.ads with one ad.',
+ fieldName: 'ads',
+ fieldValue: [{renderURL: AD1_URL}]
+});
+
+makeTest({
+ name: 'InterestGroup.ads one ad with metadata object.',
+ fieldName: 'ads',
+ fieldValue: [{renderURL: AD1_URL, metadata: {foo: 1, bar: [2, 3], baz: '4'}}]
+});
+
+makeTest({
+ name: 'InterestGroup.ads one ad with metadata string.',
+ fieldName: 'ads',
+ fieldValue: [{renderURL: AD1_URL, metadata: 'foo'}]
+});
+
+makeTest({
+ name: 'InterestGroup.ads one ad with null metadata.',
+ fieldName: 'ads',
+ fieldValue: [{renderURL: AD1_URL, metadata: null}]
+});
+
+makeTest({
+ name: 'InterestGroup.ads one ad with adRenderId. This field should not be passed to generateBid.',
+ fieldName: 'ads',
+ fieldValue: [{renderURL: AD1_URL}],
+ interestGroupOverrides: {ads: [{renderURL: AD1_URL, adRenderId: 'twelve chars'}]}
+});
+
+makeTest({
+ name: 'InterestGroup.ads one ad with buyerAndSellerReportingId. This field should not be passed to generateBid.',
+ fieldName: 'ads',
+ fieldValue: [{renderURL: AD1_URL}],
+ interestGroupOverrides: {ads: [{renderURL: AD1_URL,
+ buyerAndSellerReportingId: 'Arbitrary text'}]}
+});
+
+makeTest({
+ name: 'InterestGroup.ads one ad with buyerReportingId. This field should not be passed to generateBid.',
+ fieldName: 'ads',
+ fieldValue: [{renderURL: AD1_URL}],
+ interestGroupOverrides: {ads: [{renderURL: AD1_URL,
+ buyerReportingId: 'Arbitrary text'}]}
+});
+
+makeTest({
+ name: 'InterestGroup.ads one ad with novel field. This field should not be passed to generateBid.',
+ fieldName: 'ads',
+ fieldValue: [{renderURL: AD1_URL}],
+ interestGroupOverrides: {ads: [{renderURL: AD1_URL, novelField: 'Foo'}]}
+});
+
+makeTest({
+ name: 'InterestGroup.ads with multiple ads.',
+ fieldName: 'ads',
+ fieldValue: [{renderURL: AD1_URL, metadata: 1},
+ {renderURL: AD2_URL, metadata: [2]}],
+ interestGroupOverrides: {ads: [{renderURL: AD1_URL, metadata: 1},
+ {renderURL: AD2_URL, metadata: [2]}]}
+});
+
+// This should probably be an error. This WPT test serves to encourage there to be a
+// new join-leave WPT test when that is fixed.
+makeTest({
+ name: 'InterestGroup.ads duplicate ad.',
+ fieldName: 'ads',
+ fieldValue: [{renderURL: AD1_URL}, {renderURL: AD1_URL}],
+ interestGroupOverrides: {ads: [{renderURL: AD1_URL}, {renderURL: AD1_URL}]}
+});
+
+makeTest({
+ name: 'InterestGroup.adComponents is undefined.',
+ fieldName: 'adComponents',
+ fieldValue: undefined
+});
+
+// This one is likely a bug.
+makeTest({
+ name: 'InterestGroup.adComponents is empty array.',
+ fieldName: 'adComponents',
+ fieldValue: undefined,
+ interestGroupOverrides: {adComponents: []}
+});
+
+makeTest({
+ name: 'InterestGroup.adComponents with one ad.',
+ fieldName: 'adComponents',
+ fieldValue: [{renderURL: AD1_URL}]
+});
+
+makeTest({
+ name: 'InterestGroup.adComponents one ad with metadata object.',
+ fieldName: 'adComponents',
+ fieldValue: [{renderURL: AD1_URL, metadata: {foo: 1, bar: [2, 3], baz: '4'}}]
+});
+
+makeTest({
+ name: 'InterestGroup.adComponents one ad with metadata string.',
+ fieldName: 'adComponents',
+ fieldValue: [{renderURL: AD1_URL, metadata: 'foo'}]
+});
+
+makeTest({
+ name: 'InterestGroup.adComponents one ad with null metadata.',
+ fieldName: 'adComponents',
+ fieldValue: [{renderURL: AD1_URL, metadata: null}]
+});
+
+makeTest({
+ name: 'InterestGroup.adComponents one ad with adRenderId. This field should not be passed to generateBid.',
+ fieldName: 'adComponents',
+ fieldValue: [{renderURL: AD1_URL}],
+ interestGroupOverrides: {adComponents: [{renderURL: AD1_URL,
+ adRenderId: 'twelve chars'}]}
+});
+
+makeTest({
+ name: 'InterestGroup.adComponents one ad with buyerAndSellerReportingId. This field should not be passed to generateBid.',
+ fieldName: 'adComponents',
+ fieldValue: [{renderURL: AD1_URL}],
+ interestGroupOverrides: {adComponents: [{renderURL: AD1_URL,
+ buyerAndSellerReportingId: 'Arbitrary text'}]}
+});
+
+makeTest({
+ name: 'InterestGroup.adComponents one ad with buyerReportingId. This field should not be passed to generateBid.',
+ fieldName: 'adComponents',
+ fieldValue: [{renderURL: AD1_URL}],
+ interestGroupOverrides: {adComponents: [{renderURL: AD1_URL,
+ buyerReportingId: 'Arbitrary text'}]}
+});
+
+makeTest({
+ name: 'InterestGroup.adComponents one ad with novel field. This field should not be passed to generateBid.',
+ fieldName: 'adComponents',
+ fieldValue: [{renderURL: AD1_URL}],
+ interestGroupOverrides: {adComponents: [{renderURL: AD1_URL,
+ novelField: 'Foo'}]}
+});
+
+makeTest({
+ name: 'InterestGroup.adComponents with multiple ads.',
+ fieldName: 'adComponents',
+ fieldValue: [{renderURL: AD1_URL, metadata: 1}, {renderURL: AD2_URL, metadata: [2]}]
+});
+
+makeTest({
+ name: 'InterestGroup.auctionServerRequestFlags is undefined',
+ fieldName: 'auctionServerRequestFlags',
+ fieldValue: undefined
+});
+
+makeTest({
+ name: 'InterestGroup.auctionServerRequestFlags is "omit-ads".',
+ fieldName: 'auctionServerRequestFlags',
+ fieldValue: undefined,
+ interestGroupOverrides: {auctionServerRequestFlags: ['omit-ads']}
+});
+
+makeTest({
+ name: 'InterestGroup.auctionServerRequestFlags is "include-full-ads".',
+ fieldName: 'auctionServerRequestFlags',
+ fieldValue: undefined,
+ interestGroupOverrides: {auctionServerRequestFlags: ['include-full-ads']}
+});
+
+makeTest({
+ name: 'InterestGroup.auctionServerRequestFlags has multiple values.',
+ fieldName: 'auctionServerRequestFlags',
+ fieldValue: undefined,
+ interestGroupOverrides: {auctionServerRequestFlags: ['omit-ads', 'include-full-ads']}
+});
+
+makeTest({
+ name: 'InterestGroup.auctionServerRequestFlags.',
+ fieldName: 'auctionServerRequestFlags',
+ fieldValue: undefined,
+ interestGroupOverrides: {auctionServerRequestFlags: ['noval value']}
+});
+
+// This should probably be an error. This WPT test serves to encourage there to be a
+// new join-leave WPT test when that is fixed.
+makeTest({
+ name: 'InterestGroup.adComponents duplicate ad.',
+ fieldName: 'adComponents',
+ fieldValue: [{renderURL: AD1_URL}, {renderURL: AD1_URL}],
+ interestGroupOverrides: {adComponents: [{renderURL: AD1_URL}, {renderURL: AD1_URL}]}
+});
diff --git a/testing/web-platform/tests/fledge/tentative/join-leave-ad-interest-group-in-fenced-frame.https.window.js b/testing/web-platform/tests/fledge/tentative/join-leave-ad-interest-group-in-fenced-frame.https.window.js
new file mode 100644
index 0000000000..e6836ab2f4
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/join-leave-ad-interest-group-in-fenced-frame.https.window.js
@@ -0,0 +1,369 @@
+// META: script=/resources/testdriver.js
+// META: script=/common/utils.js
+// META: script=resources/fledge-util.sub.js
+// META: script=/common/subset-tests.js
+// META: timeout=long
+// META: variant=?1-4
+// META: variant=?5-8
+// META: variant=?9-last
+
+"use strict;"
+
+// These are separate from the other join-leave tests because these all create
+// and navigate fenced frames, which is much slower than just joining/leaving
+// interest groups, and running the occasional auction. Most tests use a
+// buyer with an origin of OTHER_ORIGIN1, so it has a distinct origin from the
+// seller and publisher.
+
+// Creates a tracker URL that's requested when a call succeeds in a fenced
+// frame.
+function createSuccessURL(uuid, origin = document.location.origin) {
+ return createTrackerURL(origin, uuid, "track_get", "success");
+}
+
+// Creates a tracker URL that's requested when a call fails in a fenced frame, with
+// the expected exception.
+function createExceptionURL(uuid, origin = document.location.origin) {
+ return createTrackerURL(origin, uuid, "track_get", "exception");
+}
+
+// Creates a tracker URL that's requested when joinAdInterestGroup() or
+// leaveAdInterestGroup() fails with an exception other than the one that's
+// expected.
+function createBadExceptionURL(uuid, origin = document.location.origin) {
+ return createTrackerURL(origin, uuid, "track_get", "bad_exception");
+}
+
+// Creates render URL that calls "navigator.leaveAdInterestGroup()" when
+// loaded, with no arguments. It then fetches a URL depending on whether it
+// threw an exception. No exception should ever be thrown when this is run
+// in an ad URL, so only fetch the "bad exception" URL on error.
+function createNoArgsTryLeaveRenderURL(uuid, origin = document.location.origin) {
+ return createRenderURL(
+ uuid,
+ `async function TryLeave() {
+ try {
+ await navigator.leaveAdInterestGroup();
+ await fetch("${createSuccessURL(uuid, origin)}");
+ } catch (e) {
+ await fetch("${createBadExceptionURL(uuid, origin)}");
+ }
+ }
+
+ TryLeave();`,
+ /*signalsParams=*/null,
+ origin);
+}
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ // Interest group that an ad fenced frame attempts to join. The join should
+ // fail.
+ let interestGroupJoinedInFrame = createInterestGroupForOrigin(
+ uuid, document.location.origin, {name: 'group2'});
+
+ // Create a render URL that tries to join "interestGroupJoinedInFrame".
+ const renderURL = createRenderURL(
+ uuid,
+ `async function TryJoin() {
+ try {
+ await navigator.joinAdInterestGroup(
+ ${JSON.stringify(interestGroupJoinedInFrame)});
+ await fetch("${createSuccessURL(uuid)}");
+ } catch (e) {
+ if (e instanceof DOMException && e.name === "NotAllowedError") {
+ await fetch("${createExceptionURL(uuid)}");
+ } else {
+ await fetch("${createBadExceptionURL(uuid)}");
+ }
+ }
+ }
+
+ TryJoin();`);
+
+ await joinInterestGroup(test, uuid, {ads: [{ renderURL: renderURL}]});
+
+ await runBasicFledgeAuctionAndNavigate(test, uuid);
+
+ // This should wait until the leave call has thrown an exception.
+ await waitForObservedRequests(
+ uuid,
+ [createBidderReportURL(uuid), createSellerReportURL(uuid), createExceptionURL(uuid)]);
+
+ // Leave the initial interest group.
+ await leaveInterestGroup();
+
+ // Check the interest group was not successfully joined in the fenced frame
+ // by running an auction, to make sure the thrown exception accurately
+ // indicates the group wasn't joined.
+ await runBasicFledgeTestExpectingNoWinner(test, uuid);
+}, 'joinAdInterestGroup() in ad fenced frame.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ // Create a render URL that tries to leave the default test interest group by
+ // name. Even a though a render URL can leave its own interest group by using
+ // the 0-argument version of leaveAdInterestGroup(), it can't leave its own
+ // interest group by using the 1-argument version, so this should fail.
+ const renderURL = createRenderURL(
+ uuid,
+ `async function TryLeave() {
+ try {
+ await navigator.leaveAdInterestGroup(
+ {owner: "${window.location.origin}", name: "${DEFAULT_INTEREST_GROUP_NAME}"});
+ await fetch("${createSuccessURL(uuid)}");
+ } catch (e) {
+ if (e instanceof DOMException && e.name === "NotAllowedError") {
+ await fetch("${createExceptionURL(uuid)}");
+ } else {
+ await fetch("${createBadExceptionURL(uuid)}");
+ }
+ }
+ }
+
+ TryLeave();`);
+
+ await joinInterestGroup(
+ test, uuid,
+ {ads: [{ renderURL: renderURL}]});
+
+ await runBasicFledgeAuctionAndNavigate(test, uuid);
+
+ // This should wait until the leave call has thrown an exception.
+ await waitForObservedRequests(
+ uuid,
+ [createBidderReportURL(uuid), createSellerReportURL(uuid), createExceptionURL(uuid)]);
+
+ // Check the interest group was not left.
+ await runBasicFledgeTestExpectingWinner(test, uuid);
+}, 'leaveAdInterestGroup() in ad fenced frame, specify an interest group.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ const bidder_origin = OTHER_ORIGIN1;
+ const render_url_origin = window.location.origin;
+
+ await joinCrossOriginInterestGroup(
+ test, uuid, bidder_origin,
+ {ads: [{ renderURL: createNoArgsTryLeaveRenderURL(uuid, render_url_origin) }]});
+
+ await runBasicFledgeAuctionAndNavigate(test, uuid, {interestGroupBuyers : [bidder_origin]});
+
+ // Leaving the interest group should claim to succeed, to avoid leaking
+ // whether or not the buyer was same-origin or to the fenced frame.
+ await waitForObservedRequests(
+ uuid,
+ [ createBidderReportURL(uuid), createSellerReportURL(uuid),
+ createSuccessURL(uuid, render_url_origin)]);
+
+ // Check the interest group was not actually left.
+ await runBasicFledgeTestExpectingWinner(test, uuid, {interestGroupBuyers : [bidder_origin]});
+}, 'leaveAdInterestGroup() in non-buyer origin ad fenced frame, no parameters.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ const bidder_origin = OTHER_ORIGIN1;
+ const render_url_origin = OTHER_ORIGIN1;
+
+ // Use a different origin for the buyer, to make sure that's the origin
+ // that matters.
+ await joinCrossOriginInterestGroup(
+ test, uuid, bidder_origin,
+ {ads: [{ renderURL: createNoArgsTryLeaveRenderURL(uuid, render_url_origin) }]});
+
+ await runBasicFledgeAuctionAndNavigate(test, uuid, {interestGroupBuyers : [bidder_origin]});
+
+ // This should wait until the leave call has completed.
+ await waitForObservedRequests(
+ uuid,
+ [ createBidderReportURL(uuid), createSellerReportURL(uuid),
+ createSuccessURL(uuid, render_url_origin)]);
+
+ // Check the interest group was actually left.
+ await runBasicFledgeTestExpectingNoWinner(test, uuid, {interestGroupBuyers : [bidder_origin]});
+}, 'leaveAdInterestGroup() in buyer origin ad fenced frame, no parameters.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ const bidder_origin = OTHER_ORIGIN1;
+ const render_url_origin = OTHER_ORIGIN1;
+ const iframe_origin = OTHER_ORIGIN1;
+
+ // Create a render URL which, in an iframe, loads the common "try leave"
+ // render URL from the buyer's origin (which isn't technically being used as
+ // a render URL, in this case).
+ const renderURL = createRenderURL(
+ uuid,
+ `let iframe = document.createElement("iframe");
+ iframe.permissions = "join-ad-interest-group";
+ iframe.src = "${createNoArgsTryLeaveRenderURL(uuid, iframe_origin)}";
+ document.body.appendChild(iframe);`,
+ /*signalsParams=*/null,
+ render_url_origin);
+
+ await joinCrossOriginInterestGroup(
+ test, uuid, bidder_origin,
+ {ads: [{ renderURL: renderURL }]});
+
+ await runBasicFledgeAuctionAndNavigate(test, uuid, {interestGroupBuyers : [bidder_origin]});
+
+ // This should wait until the leave call has completed.
+ await waitForObservedRequests(
+ uuid,
+ [ createBidderReportURL(uuid), createSellerReportURL(uuid),
+ createSuccessURL(uuid, iframe_origin)]);
+
+ // Check the interest group was actually left.
+ await runBasicFledgeTestExpectingNoWinner(test, uuid, {interestGroupBuyers : [bidder_origin]});
+}, 'leaveAdInterestGroup() in same-origin iframe inside buyer origin ad fenced frame, no parameters.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ const bidder_origin = OTHER_ORIGIN1;
+ const render_url_origin = OTHER_ORIGIN1;
+ const iframe_origin = document.location.origin;
+
+ // Create a render URL which, in an iframe, loads the common "try leave"
+ // render URL from an origin other than the buyer's origin.
+ const renderURL = createRenderURL(
+ uuid,
+ `let iframe = document.createElement("iframe");
+ iframe.permissions = "join-ad-interest-group";
+ iframe.src = "${createNoArgsTryLeaveRenderURL(uuid, iframe_origin)}";
+ document.body.appendChild(iframe);`,
+ /*signalsParams=*/null,
+ render_url_origin);
+
+ await joinCrossOriginInterestGroup(
+ test, uuid, bidder_origin,
+ {ads: [{ renderURL: renderURL }]});
+
+ await runBasicFledgeAuctionAndNavigate(test, uuid, {interestGroupBuyers : [bidder_origin]});
+
+ // Leaving the interest group should claim to succeed, to avoid leaking
+ // whether or not the buyer was same-origin or to the iframe.
+ await waitForObservedRequests(
+ uuid,
+ [ createBidderReportURL(uuid), createSellerReportURL(uuid),
+ createSuccessURL(uuid, iframe_origin)]);
+
+ // Check the interest group was not actually left.
+ await runBasicFledgeTestExpectingWinner(test, uuid, {interestGroupBuyers : [bidder_origin]});
+}, 'leaveAdInterestGroup() in cross-origin iframe inside buyer origin ad fenced frame, no parameters.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ const bidder_origin = OTHER_ORIGIN1;
+ const render_url_origin = document.location.origin;
+ const iframe_origin = document.location.origin;
+
+ // Create a render URL which, in an iframe, loads the common "try leave"
+ // render URL from an origin other than the buyer's origin (which isn't
+ // technically being used as a render URL, in this case).
+ const renderURL = createRenderURL(
+ uuid,
+ `let iframe = document.createElement("iframe");
+ iframe.permissions = "join-ad-interest-group";
+ iframe.src = "${createNoArgsTryLeaveRenderURL(uuid, iframe_origin)}";
+ document.body.appendChild(iframe);`,
+ /*signalsParams=*/null,
+ render_url_origin);
+
+ await joinCrossOriginInterestGroup(
+ test, uuid, bidder_origin,
+ {ads: [{ renderURL: renderURL }]});
+
+ await runBasicFledgeAuctionAndNavigate(test, uuid, {interestGroupBuyers : [bidder_origin]});
+
+ // Leaving the interest group should claim to succeed, to avoid leaking
+ // whether or not the buyer was same-origin or to the fenced frame.
+ await waitForObservedRequests(
+ uuid,
+ [ createBidderReportURL(uuid), createSellerReportURL(uuid),
+ createSuccessURL(uuid, iframe_origin)]);
+
+ // Check the interest group was not actually left.
+ await runBasicFledgeTestExpectingWinner(test, uuid, {interestGroupBuyers : [bidder_origin]});
+}, 'leaveAdInterestGroup() in same-origin iframe inside non-buyer origin ad fenced frame, no parameters.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ const bidder_origin = OTHER_ORIGIN1;
+ const render_url_origin = document.location.origin;
+ const iframe_origin = OTHER_ORIGIN1;
+
+ // Create a render URL which, in an iframe, loads the common "try leave"
+ // render URL from the buyer's origin (which isn't technically being used as
+ // a render URL, in this case).
+ const renderURL = createRenderURL(
+ uuid,
+ `let iframe = document.createElement("iframe");
+ iframe.permissions = "join-ad-interest-group";
+ iframe.src = "${createNoArgsTryLeaveRenderURL(uuid, iframe_origin)}";
+ document.body.appendChild(iframe);`,
+ /*signalsParams=*/null,
+ render_url_origin);
+
+ await joinCrossOriginInterestGroup(
+ test, uuid, bidder_origin,
+ {ads: [{ renderURL: renderURL }]});
+
+ await runBasicFledgeAuctionAndNavigate(test, uuid, {interestGroupBuyers : [bidder_origin]});
+ // Leaving the interest group should succeed.
+ await waitForObservedRequests(
+ uuid,
+ [ createBidderReportURL(uuid), createSellerReportURL(uuid),
+ createSuccessURL(uuid, iframe_origin)]);
+
+ // Check the interest group was left.
+ await runBasicFledgeTestExpectingNoWinner(test, uuid, {interestGroupBuyers : [bidder_origin]});
+}, 'leaveAdInterestGroup() in cross-origin buyer iframe inside non-buyer origin ad fenced frame, no parameters.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ // Render URL that loads the first ad component in a nested fenced frame.
+ let loadFirstComponentAdURL =
+ createRenderURL(
+ uuid,
+ `let fencedFrame = document.createElement("fencedframe");
+ fencedFrame.mode = "opaque-ads";
+ fencedFrame.config = window.fence.getNestedConfigs()[0];
+ document.body.appendChild(fencedFrame);`,
+ /*signalsParams=*/null,
+ OTHER_ORIGIN1);
+
+ await joinInterestGroup(
+ test, uuid,
+ // Interest group that makes a bid with a component ad. The render URL
+ // will open the component ad in a fenced frame, and the component ad
+ // URL is the common URL that tries to leave the ad's current interest
+ // group, reporting the result to a tracker URL.
+ { biddingLogicURL: createBiddingScriptURL(
+ { generateBid: `return {
+ bid: 1,
+ render: interestGroup.ads[0].renderURL,
+ adComponents: [interestGroup.adComponents[0].renderURL]
+ };` }),
+ ads: [{ renderURL: loadFirstComponentAdURL }],
+ adComponents: [{ renderURL: createNoArgsTryLeaveRenderURL(uuid) }]});
+
+ await runBasicFledgeAuctionAndNavigate(test, uuid);
+
+ // Leaving the interest group should claim to succeed, to avoid leaking
+ // whether or not the buyer was same-origin or to the fenced frame.
+ await waitForObservedRequests(
+ uuid,
+ [createSellerReportURL(uuid), createSuccessURL(uuid)]);
+
+ // Check the interest group was left.
+ await runBasicFledgeTestExpectingNoWinner(test, uuid);
+}, 'leaveAdInterestGroup() in component ad fenced frame, no parameters.');
diff --git a/testing/web-platform/tests/fledge/tentative/join-leave-ad-interest-group.https.window.js b/testing/web-platform/tests/fledge/tentative/join-leave-ad-interest-group.https.window.js
new file mode 100644
index 0000000000..b5dfe025bf
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/join-leave-ad-interest-group.https.window.js
@@ -0,0 +1,604 @@
+// META: script=/resources/testdriver.js
+// META: script=/common/utils.js
+// META: script=resources/fledge-util.sub.js
+// META: script=/common/subset-tests.js
+// META: timeout=long
+// META: variant=?1-10
+// META: variant=?11-20
+// META: variant=?21-30
+// META: variant=?31-40
+// META: variant=?41-50
+// META: variant=?51-60
+// META: variant=?61-70
+// META: variant=?71-80
+// META: variant=?81-last
+
+"use strict;"
+
+// These tests are focused on joinAdInterestGroup() and leaveAdInterestGroup().
+// Most join tests do not run auctions, but instead only check the result of
+// the returned promise, since testing that interest groups are actually
+// joined, and that each interestGroup field behaves as intended, are covered
+// by other tests.
+
+// Minimal fields needed for a valid interest group. Used in most test cases.
+const BASE_INTEREST_GROUP = {
+ owner: window.location.origin,
+ name: 'default name',
+}
+
+// Each test case attempts to join and then leave an interest group, checking
+// if any exceptions are thrown from either operation.
+const SIMPLE_JOIN_LEAVE_TEST_CASES = [
+ { expectJoinSucces: false,
+ expectLeaveSucces: false,
+ interestGroup: null
+ },
+ { expectJoinSucces: false,
+ expectLeaveSucces: false,
+ interestGroup: {}
+ },
+
+ // Basic success test case.
+ { expectJoinSucces: true,
+ expectLeaveSucces: true,
+ interestGroup: BASE_INTEREST_GROUP
+ },
+
+ // "owner" tests
+ { expectJoinSucces: false,
+ expectLeaveSucces: false,
+ interestGroup: { name: 'default name' }
+ },
+ { expectJoinSucces: false,
+ expectLeaveSucces: false,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ owner: null}
+ },
+ { expectJoinSucces: false,
+ expectLeaveSucces: false,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ owner: window.location.origin.replace('https', 'http')}
+ },
+ { expectJoinSucces: false,
+ expectLeaveSucces: false,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ owner: window.location.origin.replace('https', 'wss')}
+ },
+ // Cross-origin joins and leaves are not allowed without .well-known
+ // permissions.
+ { expectJoinSucces: false,
+ expectLeaveSucces: false,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ owner: '{{hosts[][www]}}' }
+ },
+
+ // "name" tests
+ { expectJoinSucces: false,
+ expectLeaveSucces: false,
+ interestGroup: { owner: window.location.origin }
+ },
+ { expectJoinSucces: true,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ name: ''}
+ },
+
+ // "priority" tests
+ { expectJoinSucces: true,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ priority: 1}
+ },
+ { expectJoinSucces: true,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ priority: 0}
+ },
+ { expectJoinSucces: true,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ priority: -1.5}
+ },
+
+ // "priorityVector" tests
+ { expectJoinSucces: false,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ priorityVector: null}
+ },
+ { expectJoinSucces: false,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ priorityVector: 1}
+ },
+ { expectJoinSucces: false,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ priorityVector: {a: 'apple'}}
+ },
+ { expectJoinSucces: true,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ priorityVector: {}}
+ },
+ { expectJoinSucces: true,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ priorityVector: {a: 1}}
+ },
+ { expectJoinSucces: true,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ priorityVector: {'a': 1, 'b': -4.5, 'a.b': 0}}
+ },
+
+ // "prioritySignalsOverrides" tests
+ { expectJoinSucces: false,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ prioritySignalsOverrides: null}
+ },
+ { expectJoinSucces: false,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ prioritySignalsOverrides: 1}
+ },
+ { expectJoinSucces: false,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ prioritySignalsOverrides: {a: 'apple'}}
+ },
+ { expectJoinSucces: true,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ prioritySignalsOverrides: {}}
+ },
+ { expectJoinSucces: true,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ prioritySignalsOverrides: {a: 1}}
+ },
+ { expectJoinSucces: true,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ prioritySignalsOverrides: {'a': 1, 'b': -4.5, 'a.b': 0}}
+ },
+
+ // "enableBiddingSignalsPrioritization" tests
+ { expectJoinSucces: true,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ enableBiddingSignalsPrioritization: true}
+ },
+ { expectJoinSucces: true,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ enableBiddingSignalsPrioritization: false}
+ },
+
+ // "biddingLogicURL" tests
+ { expectJoinSucces: true,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ biddingLogicURL: null }
+ },
+ { expectJoinSucces: false,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ biddingLogicURL: 'https://{{hosts[][www]}}/foo.js' }
+ },
+ { expectJoinSucces: false,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ biddingLogicURL: 'data:text/javascript,Foo' }
+ },
+ { expectJoinSucces: true,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ biddingLogicURL: `${window.location.origin}/foo.js`}
+ },
+ { expectJoinSucces: true,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ biddingLogicURL: 'relative/path' }
+ },
+
+ // "biddingWasmHelperURL" tests
+ { expectJoinSucces: true,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ biddingWasmHelperURL: null }
+ },
+ { expectJoinSucces: false,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ biddingWasmHelperURL: 'https://{{hosts[][www]}}/foo.js' }
+ },
+ { expectJoinSucces: false,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ biddingWasmHelperURL: 'data:application/wasm,Foo' }
+ },
+ { expectJoinSucces: true,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ biddingWasmHelperURL: `${window.location.origin}/foo.js`}
+ },
+ { expectJoinSucces: true,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ biddingWasmHelperURL: 'relative/path' }
+ },
+
+ // "dailyUpdateUrl" tests
+ { expectJoinSucces: true,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ dailyUpdateUrl: null }
+ },
+ { expectJoinSucces: false,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ dailyUpdateUrl: 'https://{{hosts[][www]}}/foo.js' }
+ },
+ { expectJoinSucces: false,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ dailyUpdateUrl: 'data:application/wasm,Foo' }
+ },
+ { expectJoinSucces: true,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ dailyUpdateUrl: `${window.location.origin}/foo.js`}
+ },
+ { expectJoinSucces: true,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ dailyUpdateUrl: 'relative/path' }
+ },
+
+ // "executionMode" tests
+ { expectJoinSucces: true,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ executionMode: 'compatibility' }
+ },
+ { expectJoinSucces: true,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ executionMode: 'groupByOrigin' }
+ },
+ { expectJoinSucces: true,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ executionMode: 'unknownValuesAreValid' }
+ },
+
+ // "trustedBiddingSignalsURL" tests
+ { expectJoinSucces: true,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ trustedBiddingSignalsURL: null }
+ },
+ { expectJoinSucces: false,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ trustedBiddingSignalsURL: 'https://{{hosts[][www]}}/foo.js' }
+ },
+ { expectJoinSucces: false,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ trustedBiddingSignalsURL: 'data:application/json,{}' }
+ },
+ { expectJoinSucces: true,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ trustedBiddingSignalsURL: `${window.location.origin}/foo.js`}
+ },
+ { expectJoinSucces: true,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ trustedBiddingSignalsURL: 'relative/path' }
+ },
+
+ // "trustedBiddingSignalsKeys" tests
+ { expectJoinSucces: false,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ trustedBiddingSignalsKeys: null }
+ },
+ { expectJoinSucces: false,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ trustedBiddingSignalsKeys: {}}
+ },
+ { expectJoinSucces: true,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ trustedBiddingSignalsKeys: []}
+ },
+ { expectJoinSucces: true,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ trustedBiddingSignalsKeys: ['a', 4, 'Foo']}
+ },
+
+ // "userBiddingSignals" tests
+ { expectJoinSucces: true,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ userBiddingSignals: null }
+ },
+ { expectJoinSucces: true,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ userBiddingSignals: 'foo' }
+ },
+ { expectJoinSucces: true,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ userBiddingSignals: 15 }
+ },
+ { expectJoinSucces: true,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ userBiddingSignals: [5, 'foo', [-6.4, {a: 'b'}]] }
+ },
+ { expectJoinSucces: true,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ userBiddingSignals: {a: [5, 'foo', {b: -6.4}] }}
+ },
+
+ // "ads" tests
+ { expectJoinSucces: false,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ ads: null }
+ },
+ { expectJoinSucces: false,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ ads: 5 }
+ },
+ { expectJoinSucces: false,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ ads: {} }
+ },
+ { expectJoinSucces: true,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ ads: [] }
+ },
+ { expectJoinSucces: false,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ ads: [{}] }
+ },
+ { expectJoinSucces: false,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ ads: [{metadata: [{a:'b'}, 'c'], 1:[2,3]}] }
+ },
+ { expectJoinSucces: false,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ ads: [{renderURL: 'https://somewhere.test/',
+ adRenderId: 'thirteenChars' }] }
+ },
+
+ { expectJoinSucces: true,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ ads: [{renderURL: 'https://somewhere.test/'}] }
+ },
+
+ // "adComponents" tests
+ { expectJoinSucces: false,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ adComponents: null }
+ },
+ { expectJoinSucces: false,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ adComponents: 5 }
+ },
+ { expectJoinSucces: false,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ adComponents: [{}] }
+ },
+ { expectJoinSucces: false,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ adComponents: [{metadata: [{a:'b'}, 'c'], 1:[2,3]}] }
+ },
+ { expectJoinSucces: false,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ adComponents: [{renderURL: 'https://somewhere.test/',
+ adRenderId: 'More than twelve characters'}] }
+ },
+ { expectJoinSucces: true,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ adComponents: [{renderURL: 'https://somewhere.test/'}] }
+ },
+
+ // Miscellaneous tests.
+ { expectJoinSucces: true,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ extra: false,
+ fields: {do:'not'},
+ matter: 'at',
+ all: [3,4,5] }
+ },
+
+ // Interest group dictionaries must be less than 1 MB (1048576 bytes), so
+ // test that here by using a large name on an otherwise valid interest group
+ // dictionary. The first case is the largest name value that still results in
+ // a valid dictionary, whereas the second test case produces a dictionary
+ // that's one byte too large.
+ { expectJoinSucces: true,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ name: 'a'.repeat(1048516)
+ },
+ testCaseName: "Largest possible interest group dictionary",
+ },
+ { expectJoinSucces: false,
+ expectLeaveSucces: true,
+ interestGroup: { ...BASE_INTEREST_GROUP,
+ name: 'a'.repeat(1048517)
+ },
+ testCaseName: "Oversized interest group dictionary",
+ },
+];
+
+for (testCase of SIMPLE_JOIN_LEAVE_TEST_CASES) {
+ var test_name = 'Join and leave interest group: ';
+ if ('testCaseName' in testCase) {
+ test_name += testCase.testCaseName;
+ } else {
+ test_name += JSON.stringify(testCase);
+ }
+
+ subsetTest(promise_test, (async (testCase) => {
+ const INTEREST_GROUP_LIFETIME_SECS = 1;
+
+ let join_promise = navigator.joinAdInterestGroup(testCase.interestGroup,
+ INTEREST_GROUP_LIFETIME_SECS);
+ assert_true(join_promise instanceof Promise, "join should return a promise");
+ if (testCase.expectJoinSucces) {
+ assert_equals(await join_promise, undefined);
+ } else {
+ let joinExceptionThrown = false;
+ try {
+ await join_promise;
+ } catch (e) {
+ joinExceptionThrown = true;
+ }
+ assert_true(joinExceptionThrown, 'Exception not thrown on join.');
+ }
+
+ let leave_promise = navigator.leaveAdInterestGroup(testCase.interestGroup);
+ assert_true(leave_promise instanceof Promise, "leave should return a promise");
+ if (testCase.expectLeaveSucces) {
+ assert_equals(await leave_promise, undefined);
+ } else {
+ let leaveExceptionThrown = false;
+ try {
+ await leave_promise;
+ } catch (e) {
+ leaveExceptionThrown = true;
+ }
+ assert_true(leaveExceptionThrown, 'Exception not thrown on leave.');
+ }
+ }).bind(undefined, testCase), test_name);
+}
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ // Joining an interest group without a bidding script and run an auction.
+ // There should be no winner.
+ await joinInterestGroup(test, uuid, { biddingLogicURL: null });
+ assert_equals(null, await runBasicFledgeAuction(test, uuid),
+ 'Auction unexpectedly had a winner');
+
+ // Joining an interest group with a bidding script and the same owner/name as
+ // the previously joined interest group, and re-run the auction. There should
+ // be a winner this time.
+ await joinInterestGroup(test, uuid);
+ let config = await runBasicFledgeAuction(test, uuid);
+ assert_true(config instanceof FencedFrameConfig,
+ 'Wrong value type returned from auction: ' +
+ config.constructor.name);
+
+ // Re-join the first interest group, and re-run the auction. The interest
+ // group should be overwritten again, and there should be no winner.
+ await joinInterestGroup(test, uuid, { biddingLogicURL: null });
+ assert_equals(null, await runBasicFledgeAuction(test, uuid),
+ 'Auction unexpectedly had a winner');
+}, 'Join same interest group overwrites old matching group.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ // Join an interest group, run an auction to make sure it was joined.
+ await joinInterestGroup(test, uuid);
+ let config = await runBasicFledgeAuction(test, uuid);
+ assert_true(config instanceof FencedFrameConfig,
+ 'Wrong value type returned from auction: ' +
+ config.constructor.name);
+
+ // Leave the interest group, re-run the auction. There should be no winner.
+ await leaveInterestGroup();
+ assert_equals(null, await runBasicFledgeAuction(test, uuid),
+ 'Auction unexpectedly had a winner');
+}, 'Leaving interest group actually leaves interest group.');
+
+subsetTest(promise_test, async test => {
+ // This should not throw.
+ await leaveInterestGroup({ name: 'Never join group' });
+}, 'Leave an interest group that was never joined.');
+
+///////////////////////////////////////////////////////////////////////////////
+// Expiration tests
+///////////////////////////////////////////////////////////////////////////////
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ // Joins the default interest group, with a 0.2 second duration.
+ await joinInterestGroup(test, uuid, {}, 0.2);
+
+ // Keep on running auctions until interest group duration expires.
+ // Unfortunately, there's no duration that's guaranteed to be long enough to
+ // be be able to win an auction once, but short enough to prevent this test
+ // from running too long, so can't check the interest group won at least one
+ // auction.
+ while (await runBasicFledgeAuction(test, uuid) !== null);
+}, 'Interest group duration.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ // Join interest group with a duration of -600. The interest group should
+ // immediately expire, and not be allowed to participate in auctions.
+ await joinInterestGroup(test, uuid, {}, -600);
+ assert_true(await runBasicFledgeAuction(test, uuid) === null);
+}, 'Interest group duration of -600.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ // Join a long-lived interest group.
+ await joinInterestGroup(test, uuid, {}, 600);
+
+ // Make sure interest group with a non-default timeout was joined.
+ assert_true(await runBasicFledgeAuction(test, uuid) !== null);
+
+ // Re-join interest group with a duration value of 0.2 seconds.
+ await joinInterestGroup(test, uuid, {}, 0.2);
+
+ // Keep on running auctions until interest group expires.
+ while (await runBasicFledgeAuction(test, uuid) !== null);
+}, 'Interest group test with overwritten duration.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ // Join a long-lived interest group.
+ await joinInterestGroup(test, uuid, {}, 600);
+
+ // Re-join interest group with a duration value of 0.2 seconds. The new
+ // duration should take precedence, and the interest group should immediately
+ // expire.
+ await joinInterestGroup(test, uuid, {}, -600);
+ assert_true(await runBasicFledgeAuction(test, uuid) === null);
+}, 'Interest group test with overwritten duration of -600.');
diff --git a/testing/web-platform/tests/fledge/tentative/kanon-status-below-threshold.https.window.js b/testing/web-platform/tests/fledge/tentative/kanon-status-below-threshold.https.window.js
new file mode 100644
index 0000000000..4eac4a8e91
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/kanon-status-below-threshold.https.window.js
@@ -0,0 +1,20 @@
+// META: script=/resources/testdriver.js
+// META: script=/common/utils.js
+// META: script=resources/fledge-util.sub.js
+// META: script=/common/subset-tests.js
+
+"use strict;"
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await runReportTest(
+ test, uuid,
+ { reportWinSuccessCondition:
+ `browserSignals.kAnonStatus === "belowThreshold"`,
+ reportWin:
+ `sendReportTo('${createBidderReportURL(uuid)}');` },
+ // expectedReportUrls:
+ [createBidderReportURL(uuid)]);
+ },
+ 'Check kAnonStatus is "belowThreshold" when FledgeConsiderKAnonymity' +
+ 'is enabled and FledgeEnforceKAnonymity is disabled');
diff --git a/testing/web-platform/tests/fledge/tentative/kanon-status-not-calculated.https.window.js b/testing/web-platform/tests/fledge/tentative/kanon-status-not-calculated.https.window.js
new file mode 100644
index 0000000000..a3ac19bd85
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/kanon-status-not-calculated.https.window.js
@@ -0,0 +1,20 @@
+// META: script=/resources/testdriver.js
+// META: script=/common/utils.js
+// META: script=resources/fledge-util.sub.js
+// META: script=/common/subset-tests.js
+
+"use strict;"
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await runReportTest(
+ test, uuid,
+ { reportWinSuccessCondition:
+ `browserSignals.kAnonStatus === "notCalculated"`,
+ reportWin:
+ `sendReportTo('${createBidderReportURL(uuid)}');` },
+ // expectedReportUrls:
+ [createBidderReportURL(uuid)]);
+ },
+ 'Check kAnonStatus is "notCalculated" when FledgeConsiderKAnonymity' +
+ 'and FledgeEnforceKAnonymity are both disabled');
diff --git a/testing/web-platform/tests/fledge/tentative/network.https.window.js b/testing/web-platform/tests/fledge/tentative/network.https.window.js
new file mode 100644
index 0000000000..fe287767c8
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/network.https.window.js
@@ -0,0 +1,327 @@
+// META: script=/resources/testdriver.js
+// META: script=/resources/testdriver-vendor.js
+// META: script=/common/subset-tests.js
+// META: script=/common/utils.js
+// META: script=resources/fledge-util.sub.js
+// META: timeout=long
+// META: variant=?1-5
+// META: variant=?6-10
+// META: variant=?11-last
+
+"use strict";
+
+// These tests focus on Protected Audience network requests - make sure they
+// have no cookies, can set no cookies, and otherwise behave in the expected
+// manner as related to the fetch spec. These tests don't cover additional
+// request or response headers specific to Protected Audience API.
+
+// URL that sets a cookie named "cookie" with a value of "cookie".
+const SET_COOKIE_URL = `${BASE_URL}resources/set-cookie.asis`;
+
+// URL that redirects to trusted bidding or scoring signals, depending on the
+// query parameters, maintaining the query parameters for the redirect.
+const REDIRECT_TO_TRUSTED_SIGNALS_URL = `${BASE_URL}resources/redirect-to-trusted-signals.py`;
+
+// Returns a URL that stores request headers. Headers can later be retrieved
+// as a name-to-list-of-values mapping with
+// "(await fetchTrackedData(uuid)).trackedHeaders"
+function createHeaderTrackerURL(uuid) {
+ return createTrackerURL(window.location.origin, uuid, 'track_headers');
+}
+
+// Returns a URL that redirects to the provided URL. Uses query strings, so
+// not suitable for generating trusted bidding/scoring signals URLs.
+function createRedirectURL(location) {
+ let url = new URL(`${BASE_URL}resources/redirect.py`);
+ url.searchParams.append('location', location);
+ return url.toString();
+}
+
+// Delete all cookies. Separate function so that can be replaced with
+// something else for testing outside of a WPT environment.
+async function deleteAllCookies() {
+ await test_driver.delete_all_cookies();
+}
+
+// Deletes all cookies (to avoid pre-existing cookies causing inconsistent
+// output on failure) and sets a cookie with name "cookie" and a value of
+// "cookie". Adds a cleanup task to delete all cookies again when the test
+// is done.
+async function setCookie(test) {
+ await deleteAllCookies();
+ document.cookie = 'cookie=cookie; path=/'
+ test.add_cleanup(deleteAllCookies);
+}
+
+// Assert that "headers" has a single header with "name", whose value is "value".
+function assertHasHeader(headers, name, value) {
+ assert_equals(JSON.stringify(headers[name]), JSON.stringify([value]),
+ 'Header ' + name);
+}
+
+// Assert that "headers" has no header with "name"
+function assertDoesNotHaveHeader(headers, name) {
+ assert_equals(headers[name], undefined, 'Header ' + name);
+}
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await setCookie(test);
+
+ await joinGroupAndRunBasicFledgeTestExpectingNoWinner(
+ test,
+ { uuid: uuid,
+ interestGroupOverrides: { biddingLogicURL: createHeaderTrackerURL(uuid) }
+ });
+
+ let headers = (await fetchTrackedData(uuid)).trackedHeaders;
+ assertHasHeader(headers, 'accept', 'application/javascript');
+ assertHasHeader(headers, 'sec-fetch-dest', 'empty');
+ assertHasHeader(headers, 'sec-fetch-mode', 'no-cors');
+ assertHasHeader(headers, 'sec-fetch-site', 'same-origin');
+ assertDoesNotHaveHeader(headers, 'cookie');
+ assertDoesNotHaveHeader(headers, 'referer');
+}, 'biddingLogicURL request headers.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await deleteAllCookies();
+
+ await joinGroupAndRunBasicFledgeTestExpectingNoWinner(
+ test,
+ { uuid: uuid,
+ interestGroupOverrides: { biddingLogicURL: SET_COOKIE_URL }
+ });
+
+ assert_equals(document.cookie, '');
+ await deleteAllCookies();
+}, 'biddingLogicURL Set-Cookie.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await joinGroupAndRunBasicFledgeTestExpectingNoWinner(
+ test,
+ { uuid: uuid,
+ interestGroupOverrides: {
+ biddingLogicURL: createRedirectURL(createBiddingScriptURL()) }
+ });
+}, 'biddingLogicURL redirect.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await setCookie(test);
+
+ await joinGroupAndRunBasicFledgeTestExpectingNoWinner(
+ test,
+ { uuid: uuid,
+ interestGroupOverrides: { biddingWasmHelperURL: createHeaderTrackerURL(uuid) }
+ });
+
+ let headers = (await fetchTrackedData(uuid)).trackedHeaders;
+ assertHasHeader(headers, 'accept', 'application/wasm');
+ assertHasHeader(headers, 'sec-fetch-dest', 'empty');
+ assertHasHeader(headers, 'sec-fetch-mode', 'no-cors');
+ assertHasHeader(headers, 'sec-fetch-site', 'same-origin');
+ assertDoesNotHaveHeader(headers, 'cookie');
+ assertDoesNotHaveHeader(headers, 'referer');
+}, 'biddingWasmHelperURL request headers.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await deleteAllCookies();
+
+ await joinGroupAndRunBasicFledgeTestExpectingNoWinner(
+ test,
+ { uuid: uuid,
+ interestGroupOverrides: { biddingWasmHelperURL: SET_COOKIE_URL }
+ });
+
+ assert_equals(document.cookie, '');
+ await deleteAllCookies();
+}, 'biddingWasmHelperURL Set-Cookie.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await joinGroupAndRunBasicFledgeTestExpectingNoWinner(
+ test,
+ { uuid: uuid,
+ interestGroupOverrides:
+ { biddingWasmHelperURL: createRedirectURL(createBiddingWasmHelperURL()) }
+ });
+}, 'biddingWasmHelperURL redirect.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await setCookie(test);
+
+ await joinGroupAndRunBasicFledgeTestExpectingNoWinner(
+ test,
+ { uuid: uuid,
+ auctionConfigOverrides: { decisionLogicURL: createHeaderTrackerURL(uuid) }
+ });
+
+ let headers = (await fetchTrackedData(uuid)).trackedHeaders;
+ assertHasHeader(headers, 'accept', 'application/javascript');
+ assertHasHeader(headers, 'sec-fetch-dest', 'empty');
+ assertHasHeader(headers, 'sec-fetch-mode', 'no-cors');
+ assertHasHeader(headers, 'sec-fetch-site', 'same-origin');
+ assertDoesNotHaveHeader(headers, 'cookie');
+ assertDoesNotHaveHeader(headers, 'referer');
+}, 'decisionLogicURL request headers.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await deleteAllCookies();
+
+ await joinGroupAndRunBasicFledgeTestExpectingNoWinner(
+ test,
+ { uuid: uuid,
+ auctionConfigOverrides: { decisionLogicURL: SET_COOKIE_URL }
+ });
+
+ assert_equals(document.cookie, '');
+ await deleteAllCookies();
+}, 'decisionLogicURL Set-Cookie.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await joinGroupAndRunBasicFledgeTestExpectingNoWinner(
+ test,
+ { uuid: uuid,
+ auctionConfigOverrides:
+ { decisionLogicURL: createRedirectURL(createDecisionScriptURL(uuid)) }
+ });
+}, 'decisionLogicURL redirect.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await setCookie(test);
+
+ await joinGroupAndRunBasicFledgeTestExpectingWinner(
+ test,
+ { uuid: uuid,
+ interestGroupOverrides: {
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL,
+ trustedBiddingSignalsKeys: ['headers'],
+ biddingLogicURL: createBiddingScriptURL({
+ generateBid:
+ `let headers = trustedBiddingSignals.headers;
+ function checkHeader(name, value) {
+ jsonActualValue = JSON.stringify(headers[name]);
+ if (jsonActualValue !== JSON.stringify([value]))
+ throw "Unexpected " + name + ": " + jsonActualValue;
+ }
+ checkHeader("accept", "application/json");
+ checkHeader("sec-fetch-dest", "empty");
+ checkHeader("sec-fetch-mode", "no-cors");
+ checkHeader("sec-fetch-site", "same-origin");
+ if (headers.cookie !== undefined)
+ throw "Unexpected cookie: " + JSON.stringify(headers.cookie);
+ if (headers.referer !== undefined)
+ throw "Unexpected referer: " + JSON.stringify(headers.referer);`,
+ })
+ }
+ });
+}, 'trustedBiddingSignalsURL request headers.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await deleteAllCookies();
+
+ await joinGroupAndRunBasicFledgeTestExpectingWinner(
+ test,
+ { uuid: uuid,
+ interestGroupOverrides: { trustedBiddingSignalsURL: SET_COOKIE_URL }
+ });
+
+ assert_equals(document.cookie, '');
+ await deleteAllCookies();
+}, 'trustedBiddingSignalsURL Set-Cookie.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ await joinGroupAndRunBasicFledgeTestExpectingWinner(
+ test,
+ { uuid: uuid,
+ interestGroupOverrides: {
+ trustedBiddingSignalsURL: REDIRECT_TO_TRUSTED_SIGNALS_URL,
+ trustedBiddingSignalsKeys: ['num-value'],
+ biddingLogicURL: createBiddingScriptURL({
+ generateBid:
+ `// The redirect should not be followed, so no signals should be received.
+ if (trustedBiddingSignals !== null)
+ throw "Unexpected trustedBiddingSignals: " + JSON.stringify(trustedBiddingSignals);`
+ })
+ }
+ });
+}, 'trustedBiddingSignalsURL redirect.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await setCookie(test);
+
+ let renderURL = createRenderURL(uuid, /*script=*/null, /*signalsParam=*/'headers');
+
+ await joinGroupAndRunBasicFledgeTestExpectingWinner(
+ test,
+ { uuid: uuid,
+ interestGroupOverrides: {
+ ads: [{ renderURL: renderURL }]
+ },
+ auctionConfigOverrides: {
+ trustedScoringSignalsURL: TRUSTED_SCORING_SIGNALS_URL,
+ decisionLogicURL: createDecisionScriptURL(uuid,
+ {
+ scoreAd:
+ `let headers = trustedScoringSignals.renderURL["${renderURL}"];
+ function checkHeader(name, value) {
+ jsonActualValue = JSON.stringify(headers[name]);
+ if (jsonActualValue !== JSON.stringify([value]))
+ throw "Unexpected " + name + ": " + jsonActualValue;
+ }
+ checkHeader("accept", "application/json");
+ checkHeader("sec-fetch-dest", "empty");
+ checkHeader("sec-fetch-mode", "no-cors");
+ checkHeader("sec-fetch-site", "same-origin");
+ if (headers.cookie !== undefined)
+ throw "Unexpected cookie: " + JSON.stringify(headers.cookie);
+ if (headers.referer !== undefined)
+ throw "Unexpected referer: " + JSON.stringify(headers.referer);`,
+ })
+ }
+ });
+}, 'trustedScoringSignalsURL request headers.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await deleteAllCookies();
+
+ await joinGroupAndRunBasicFledgeTestExpectingWinner(
+ test,
+ { uuid: uuid,
+ auctionConfigOverrides: { trustedScoringSignalsURL: SET_COOKIE_URL }
+ });
+
+ assert_equals(document.cookie, '');
+ await deleteAllCookies();
+}, 'trustedScoringSignalsURL Set-Cookie.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ await joinGroupAndRunBasicFledgeTestExpectingWinner(
+ test,
+ { uuid: uuid,
+ auctionConfigOverrides: {
+ trustedScoringSignalsURL: REDIRECT_TO_TRUSTED_SIGNALS_URL,
+ decisionLogicURL: createDecisionScriptURL(uuid,
+ {
+ scoreAd:
+ `// The redirect should not be followed, so no signals should be received.
+ if (trustedScoringSignals !== null)
+ throw "Unexpected trustedScoringSignals: " + JSON.stringify(trustedScoringSignals);`
+ })
+ }
+ });
+}, 'trustedScoringSignalsURL redirect.');
diff --git a/testing/web-platform/tests/fledge/tentative/no-winner.https.window.js b/testing/web-platform/tests/fledge/tentative/no-winner.https.window.js
new file mode 100644
index 0000000000..6e02139c81
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/no-winner.https.window.js
@@ -0,0 +1,106 @@
+// META: script=/resources/testdriver.js
+// META: script=/common/utils.js
+// META: script=resources/fledge-util.sub.js
+// META: script=/common/subset-tests.js
+// META: timeout=long
+// META: variant=?1-5
+// META: variant=?6-10
+// META: variant=?11-15
+// META: variant=?16-20
+// META: variant=?21-25
+// META: variant=?26-30
+// META: variant=?31-35
+// META: variant=?36-40
+// META: variant=?41-45
+// META: variant=?46-last
+
+"use strict;"
+
+// The tests in this file focus on simple auctions (one bidder, one seller, one
+// origin, one frame) which have no winning bid, either due to errors or due to
+// there being no bids, except where tests fit better with another set of tests.
+
+// Errors common to Protected Audiences network requests. These strings will be
+// appended to URLs to make the Python scripts that generate responses respond
+// with errors.
+const COMMON_NETWORK_ERRORS = [
+ 'error=close-connection',
+ 'error=http-error',
+ 'error=no-content-type',
+ 'error=wrong-content-type',
+ 'error=bad-allow-fledge',
+ 'error=fledge-not-allowed',
+ 'error=no-allow-fledge',
+ 'error=no-body',
+];
+
+const BIDDING_LOGIC_SCRIPT_ERRORS = [
+ ...COMMON_NETWORK_ERRORS,
+ 'error=no-generateBid',
+ 'generateBid=throw 1;',
+ 'generateBid=This does not compile',
+ // Default timeout test. Doesn't check how long timing out takes.
+ 'generateBid=while(1);',
+ // Bad return values:
+ 'generateBid=return 5;',
+ 'generateBid=return "Foo";',
+ 'generateBid=return interestGroup.ads[0].renderURL;',
+ 'generateBid=return {bid: 1, render: "https://not-in-ads-array.test/"};',
+ 'generateBid=return {bid: 1};',
+ 'generateBid=return {render: interestGroup.ads[0].renderURL};',
+ // These are not bidding rather than errors.
+ 'generateBid=return {bid:0, render: interestGroup.ads[0].renderURL};',
+ 'generateBid=return {bid:-1, render: interestGroup.ads[0].renderURL};'
+];
+
+const DECISION_LOGIC_SCRIPT_ERRORS = [
+ ...COMMON_NETWORK_ERRORS,
+ 'error=no-scoreAd',
+ 'scoreAd=throw 1;',
+ 'scoreAd=This does not compile',
+ // Default timeout test. Doesn't check how long timing out takes.
+ 'scoreAd=while(1);',
+ // Bad return values:
+ 'scoreAd=return "Foo";',
+ 'scoreAd=return {desirability: "Foo"};',
+ // These are rejecting the bid rather than errors.
+ 'scoreAd=return 0;',
+ 'scoreAd=return -1;',
+ 'scoreAd=return {desirability: 0};',
+ 'scoreAd=return {desirability: -1};'
+];
+
+const BIDDING_WASM_HELPER_ERRORS = [
+ ...COMMON_NETWORK_ERRORS,
+ 'error=not-wasm'
+];
+
+for (error of BIDDING_LOGIC_SCRIPT_ERRORS) {
+ subsetTest(promise_test, (async (error, test) => {
+ let biddingLogicURL = `${BASE_URL}resources/bidding-logic.sub.py?${error}`;
+ await joinGroupAndRunBasicFledgeTestExpectingNoWinner(
+ test,
+ {interestGroupOverrides: {biddingLogicURL: biddingLogicURL}}
+ );
+ }).bind(undefined, error), `Bidding logic script: ${error}`);
+}
+
+for (error of DECISION_LOGIC_SCRIPT_ERRORS) {
+ subsetTest(promise_test, (async (error, test) => {
+ let decisionLogicURL =
+ `${BASE_URL}resources/decision-logic.sub.py?${error}`;
+ await joinGroupAndRunBasicFledgeTestExpectingNoWinner(
+ test, { auctionConfigOverrides: { decisionLogicURL: decisionLogicURL } }
+ );
+ }).bind(undefined, error), `Decision logic script: ${error}`);
+}
+
+for (error of BIDDING_WASM_HELPER_ERRORS) {
+ subsetTest(promise_test, (async (error, test) => {
+ let biddingWasmHelperURL =
+ `${BASE_URL}resources/wasm-helper.py?${error}`;
+ await joinGroupAndRunBasicFledgeTestExpectingNoWinner(
+ test, { interestGroupOverrides: { biddingWasmHelperURL: biddingWasmHelperURL } }
+ );
+ }).bind(undefined, error), `Bidding WASM helper: ${error}`);
+}
diff --git a/testing/web-platform/tests/fledge/tentative/register-ad-beacon.https.window.js b/testing/web-platform/tests/fledge/tentative/register-ad-beacon.https.window.js
new file mode 100644
index 0000000000..19fab2ac1b
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/register-ad-beacon.https.window.js
@@ -0,0 +1,307 @@
+// META: script=/resources/testdriver.js
+// META: script=/common/utils.js
+// META: script=resources/fledge-util.sub.js
+// META: script=/common/subset-tests.js
+// META: timeout=long
+// META: variant=?1-5
+// META: variant=?6-10
+// META: variant=?11-last
+
+"use strict;"
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await runReportTest(
+ test, uuid,
+ { reportResult:
+ `registerAdBeacon({beacon: '${createSellerBeaconURL(uuid)}'});`,
+ reportWin:
+ '' },
+ // expectedReportUrls:
+ [`${createSellerBeaconURL(uuid)}, body: `],
+ // renderUrlOverride:
+ createRenderURL(
+ uuid,
+ `window.fence.reportEvent({
+ eventType: "beacon",
+ eventData: "",
+ destination: ["seller"]
+ });`)
+ );
+}, 'Seller calls registerAdBeacon().');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await runReportTest(
+ test, uuid,
+ { reportResult:
+ '',
+ reportWin:
+ `registerAdBeacon({beacon: '${createBidderBeaconURL(uuid)}'});`
+ },
+ // expectedReportUrls:
+ [`${createBidderBeaconURL(uuid)}, body: `],
+ // renderUrlOverride:
+ createRenderURL(
+ uuid,
+ `window.fence.reportEvent({
+ eventType: "beacon",
+ eventData: "",
+ destination: ["buyer"]
+ });`)
+ );
+}, 'Buyer calls registerAdBeacon().');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await runReportTest(
+ test, uuid,
+ { reportResult:
+ `registerAdBeacon({beacon: '${createSellerBeaconURL(uuid)}'});`,
+ reportWin:
+ '' },
+ // expectedReportUrls:
+ [`${createSellerBeaconURL(uuid)}, body: body`],
+ // renderUrlOverride:
+ createRenderURL(
+ uuid,
+ `window.fence.reportEvent({
+ eventType: "beacon",
+ eventData: "body",
+ destination: ["seller"]
+ });`)
+ );
+}, 'Seller calls registerAdBeacon(), beacon sent with body.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await runReportTest(
+ test, uuid,
+ { reportResult:
+ '',
+ reportWin:
+ `registerAdBeacon({beacon: '${createBidderBeaconURL(uuid)}'});` },
+ // expectedReportUrls:
+ [`${createBidderBeaconURL(uuid)}, body: body`],
+ // renderUrlOverride:
+ createRenderURL(
+ uuid,
+ `window.fence.reportEvent({
+ eventType: "beacon",
+ eventData: "body",
+ destination: ["buyer"]
+ });`)
+ );
+}, 'Buyer calls registerAdBeacon(), beacon sent with body.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await runReportTest(
+ test, uuid,
+ { reportResult:
+ `registerAdBeacon({beacon: '${createSellerBeaconURL(uuid)}'});`,
+ reportWin:
+ '' },
+ // expectedReportUrls:
+ [`${createSellerBeaconURL(uuid)}, body: body1`,
+ `${createSellerBeaconURL(uuid)}, body: body2`],
+ // renderUrlOverride:
+ createRenderURL(
+ uuid,
+ `window.fence.reportEvent({
+ eventType: "beacon",
+ eventData: "body1",
+ destination: ["seller"]
+ });
+ window.fence.reportEvent({
+ eventType: "beacon",
+ eventData: "body2",
+ destination: ["seller"]
+ });`)
+ );
+}, 'Seller calls registerAdBeacon(). reportEvent() called twice.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await runReportTest(
+ test, uuid,
+ { reportResult:
+ '',
+ reportWin:
+ `registerAdBeacon({beacon: '${createBidderBeaconURL(uuid)}'});` },
+ // expectedReportUrls:
+ [`${createBidderBeaconURL(uuid)}, body: body1`,
+ `${createBidderBeaconURL(uuid)}, body: body2`],
+ // renderUrlOverride:
+ createRenderURL(
+ uuid,
+ `window.fence.reportEvent({
+ eventType: "beacon",
+ eventData: "body1",
+ destination: ["buyer"]
+ });
+ window.fence.reportEvent({
+ eventType: "beacon",
+ eventData: "body2",
+ destination: ["buyer"]
+ });`)
+ );
+}, 'Buyer calls registerAdBeacon(). reportEvent() called twice.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await runReportTest(
+ test, uuid,
+ { reportResult:
+ `registerAdBeacon({beacon1: '${createSellerBeaconURL(uuid, '1')}',
+ beacon2: '${createSellerBeaconURL(uuid, '2')}'});`,
+ reportWin:
+ '' },
+ // expectedReportUrls:
+ [`${createSellerBeaconURL(uuid, '1')}, body: body1`,
+ `${createSellerBeaconURL(uuid, '2')}, body: body2`],
+ // renderUrlOverride:
+ createRenderURL(
+ uuid,
+ `window.fence.reportEvent({
+ eventType: "beacon1",
+ eventData: "body1",
+ destination: ["seller"]
+ });
+ window.fence.reportEvent({
+ eventType: "beacon2",
+ eventData: "body2",
+ destination: ["seller"]
+ });`)
+ );
+}, 'Seller calls registerAdBeacon() with multiple beacons.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await runReportTest(
+ test, uuid,
+ { reportResult:
+ '',
+ reportWin:
+ `registerAdBeacon({beacon1: '${createBidderBeaconURL(uuid, '1')}',
+ beacon2: '${createBidderBeaconURL(uuid, '2')}'});`
+ },
+ // expectedReportUrls:
+ [`${createBidderBeaconURL(uuid, '1')}, body: body1`,
+ `${createBidderBeaconURL(uuid, '2')}, body: body2`],
+ // renderUrlOverride:
+ createRenderURL(
+ uuid,
+ `window.fence.reportEvent({
+ eventType: "beacon1",
+ eventData: "body1",
+ destination: ["buyer"]
+ });
+ window.fence.reportEvent({
+ eventType: "beacon2",
+ eventData: "body2",
+ destination: ["buyer"]
+ });`)
+ );
+}, 'Buyer calls registerAdBeacon() with multiple beacons.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await runReportTest(
+ test, uuid,
+ { reportResult:
+ `registerAdBeacon({beacon: '${createSellerBeaconURL(uuid)}'});`,
+ reportWin:
+ `registerAdBeacon({beacon: '${createBidderBeaconURL(uuid)}'});` },
+ // expectedReportUrls:
+ [`${createSellerBeaconURL(uuid)}, body: body`,
+ `${createBidderBeaconURL(uuid)}, body: body`],
+ // renderUrlOverride:
+ createRenderURL(
+ uuid,
+ `window.fence.reportEvent({
+ eventType: "beacon",
+ eventData: "body",
+ destination: ["seller","buyer"]
+ });`)
+ );
+}, 'Seller and buyer call registerAdBeacon() with shared reportEvent() call.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await runReportTest(
+ test, uuid,
+ { reportResult:
+ `registerAdBeacon({beacon: '${createSellerBeaconURL(uuid)}'});`,
+ reportWin:
+ `registerAdBeacon({beacon: '${createBidderBeaconURL(uuid)}'});` },
+ // expectedReportUrls:
+ [`${createSellerBeaconURL(uuid)}, body: body1`,
+ `${createBidderBeaconURL(uuid)}, body: body2`],
+ // renderUrlOverride:
+ createRenderURL(
+ uuid,
+ `window.fence.reportEvent({
+ eventType: "beacon",
+ eventData: "body1",
+ destination: ["seller"]
+ });
+ window.fence.reportEvent({
+ eventType: "beacon",
+ eventData: "body2",
+ destination: ["buyer"]
+ });`)
+ );
+}, 'Seller and buyer call registerAdBeacon() with separate reportEvent() calls.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await runReportTest(
+ test, uuid,
+ { reportResult:
+ // Multiple registerAdBeacon() call should result in an exception,
+ // throwing away all beacons and other types of reports.
+ `sendReportTo('${createSellerReportURL(uuid)}');
+ registerAdBeacon({beacon: '${createSellerBeaconURL(uuid)}'});
+ registerAdBeacon({beacon1: '${createSellerBeaconURL(uuid)}'});`,
+ reportWinSuccessCondition:
+ 'sellerSignals === null',
+ reportWin:
+ `registerAdBeacon({beacon: '${createBidderBeaconURL(uuid)}'});` },
+ // expectedReportUrls:
+ [`${createBidderBeaconURL(uuid)}, body: body`],
+ // renderUrlOverride:
+ createRenderURL(
+ uuid,
+ `window.fence.reportEvent({
+ eventType: "beacon",
+ eventData: "body",
+ destination: ["seller","buyer"]
+ });`)
+ );
+}, 'Seller calls registerAdBeacon() multiple times.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await runReportTest(
+ test, uuid,
+ { reportResult:
+ `registerAdBeacon({beacon: '${createSellerBeaconURL(uuid)}'});`,
+ reportWin:
+ // Multiple registerAdBeacon() call should result in an exception,
+ // throwing away all beacons and other types of reports.
+ `sendReportTo('${createBidderReportURL(uuid)}');
+ registerAdBeacon({beacon: '${createBidderBeaconURL(uuid)}'});
+ registerAdBeacon({beacon1: '${createBidderBeaconURL(uuid)}'});` },
+ // expectedReportUrls:
+ [`${createSellerBeaconURL(uuid)}, body: body`],
+ // renderUrlOverride:
+ createRenderURL(
+ uuid,
+ `window.fence.reportEvent({
+ eventType: "beacon",
+ eventData: "body",
+ destination: ["seller","buyer"]
+ });`)
+ );
+}, 'Buyer calls registerAdBeacon() multiple times.');
diff --git a/testing/web-platform/tests/fledge/tentative/reporting-arguments.https.window.js b/testing/web-platform/tests/fledge/tentative/reporting-arguments.https.window.js
new file mode 100644
index 0000000000..f26a969328
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/reporting-arguments.https.window.js
@@ -0,0 +1,305 @@
+// META: script=/resources/testdriver.js
+// META: script=/common/utils.js
+// META: script=resources/fledge-util.sub.js
+// META: script=/common/subset-tests.js
+// META: timeout=long
+// META: variant=?1-5
+// META: variant=?6-10
+// META: variant=?11-15
+// META: variant=?16-last
+
+"use strict;"
+
+// Simplified version of reportTest() for validating arguments to reporting
+// methods. Only takes expressions to check in reporting methods. "uuid" is
+// optional, and one is generated if not passed one.
+async function runReportArgumentValidationTest(
+ test, reportResultSuccessCondition, reportWinSuccessCondition, uuid) {
+ if (!uuid)
+ uuid = generateUuid(test);
+ await runReportTest(
+ test, uuid,
+ { reportResultSuccessCondition:
+ reportResultSuccessCondition,
+ reportResult:
+ `sendReportTo('${createSellerReportURL(uuid)}');`,
+ reportWinSuccessCondition:
+ reportWinSuccessCondition,
+ reportWin:
+ `sendReportTo('${createBidderReportURL(uuid)}');` },
+ [createSellerReportURL(uuid), createBidderReportURL(uuid)]
+ );
+}
+
+/////////////////////////////////////////////////////////////////////
+// reportResult() to reportWin() message passing tests
+/////////////////////////////////////////////////////////////////////
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await runReportTest(
+ test, uuid,
+ { reportResult:
+ `sendReportTo('${createSellerReportURL(uuid)}');
+ return 45;`,
+ reportWinSuccessCondition:
+ 'sellerSignals === 45',
+ reportWin:
+ `sendReportTo('${createBidderReportURL(uuid)}');` },
+ // expectedReportUrls:
+ [createSellerReportURL(uuid), createBidderReportURL(uuid)]
+ );
+}, 'Seller passes number to bidder.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await runReportTest(
+ test, uuid,
+ { reportResult:
+ `sendReportTo('${createSellerReportURL(uuid)}');
+ return 'foo';`,
+ reportWinSuccessCondition:
+ 'sellerSignals === "foo"',
+ reportWin:
+ `sendReportTo('${createBidderReportURL(uuid)}');` },
+ // expectedReportUrls:
+ [createSellerReportURL(uuid), createBidderReportURL(uuid)]
+ );
+}, 'Seller passes string to bidder.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await runReportTest(
+ test, uuid,
+ { reportResult:
+ `sendReportTo('${createSellerReportURL(uuid)}');
+ return [3, 1, 2];`,
+ reportWinSuccessCondition:
+ 'JSON.stringify(sellerSignals) === "[3,1,2]"',
+ reportWin:
+ `sendReportTo('${createBidderReportURL(uuid)}');` },
+ // expectedReportUrls:
+ [createSellerReportURL(uuid), createBidderReportURL(uuid)]
+ );
+}, 'Seller passes array to bidder.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await runReportTest(
+ test, uuid,
+ { reportResult:
+ `sendReportTo('${createSellerReportURL(uuid)}');
+ return {a: 4, b:['c', null, {}]};`,
+ reportWinSuccessCondition:
+ `JSON.stringify(sellerSignals) === '{"a":4,"b":["c",null,{}]}'`,
+ reportWin:
+ `sendReportTo('${createBidderReportURL(uuid)}');` },
+ // expectedReportUrls:
+ [createSellerReportURL(uuid), createBidderReportURL(uuid)]
+ );
+}, 'Seller passes object to bidder.');
+
+/////////////////////////////////////////////////////////////////////
+// reportResult() / reportWin() browserSignals tests.
+/////////////////////////////////////////////////////////////////////
+
+subsetTest(promise_test, async test => {
+ await runReportArgumentValidationTest(
+ test,
+ // reportResultSuccessCondition:
+ `browserSignals.topWindowHostname === "${window.location.hostname}"`,
+ // reportWinSuccessCondition:
+ `browserSignals.topWindowHostname === "${window.location.hostname}"`
+ );
+}, 'browserSignals.topWindowHostname test.');
+
+subsetTest(promise_test, async test => {
+ await runReportArgumentValidationTest(
+ test,
+ // reportResultSuccessCondition:
+ `browserSignals.seller === undefined`,
+ // reportWinSuccessCondition:
+ `browserSignals.seller === "${window.location.origin}"`
+ );
+}, 'browserSignals.seller test.');
+
+subsetTest(promise_test, async test => {
+ await runReportArgumentValidationTest(
+ test,
+ // reportResultSuccessCondition:
+ `browserSignals.topLevelSeller === undefined &&
+ browserSignals.componentSeller === undefined`,
+ // reportWinSuccessCondition:
+ `browserSignals.topLevelSeller === undefined &&
+ browserSignals.componentSeller === undefined`
+ );
+}, 'browserSignals.topLevelSeller and browserSignals.componentSeller test.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await runReportArgumentValidationTest(
+ test,
+ // reportResultSuccessCondition:
+ `browserSignals.renderURL === "${createRenderURL(uuid)}"`,
+ // reportWinSuccessCondition:
+ `browserSignals.renderURL === "${createRenderURL(uuid)}"`,
+ uuid
+ );
+}, 'browserSignals.renderURL test.');
+
+subsetTest(promise_test, async test => {
+ await runReportArgumentValidationTest(
+ test,
+ // reportResultSuccessCondition:
+ `browserSignals.bid === 9`,
+ // reportWinSuccessCondition:
+ `browserSignals.bid === 9`
+ );
+}, 'browserSignals.bid test.');
+
+subsetTest(promise_test, async test => {
+ await runReportArgumentValidationTest(
+ test,
+ // reportResultSuccessCondition:
+ `browserSignals.desirability === 18`,
+ // reportWinSuccessCondition:
+ `browserSignals.desirability === undefined`
+ );
+}, 'browserSignals.desirability test.');
+
+subsetTest(promise_test, async test => {
+ await runReportArgumentValidationTest(
+ test,
+ // reportResultSuccessCondition:
+ `browserSignals.topLevelSellerSignals === undefined`,
+ // reportWinSuccessCondition:
+ `browserSignals.topLevelSellerSignals === undefined`
+ );
+}, 'browserSignals.topLevelSellerSignals test.');
+
+subsetTest(promise_test, async test => {
+ await runReportArgumentValidationTest(
+ test,
+ // reportResultSuccessCondition:
+ `browserSignals.dataVersion === undefined`,
+ // reportWinSuccessCondition:
+ `browserSignals.dataVersion === undefined`
+ );
+}, 'browserSignals.dataVersion test.');
+
+subsetTest(promise_test, async test => {
+ await runReportArgumentValidationTest(
+ test,
+ // reportResultSuccessCondition:
+ `browserSignals.modifiedBid === undefined`,
+ // reportWinSuccessCondition:
+ `browserSignals.modifiedBid === undefined`
+ );
+}, 'browserSignals.modifiedBid test.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await runReportArgumentValidationTest(
+ test,
+ // reportResultSuccessCondition:
+ `browserSignals.highestScoringOtherBid === 0`,
+ // reportWinSuccessCondition:
+ `browserSignals.highestScoringOtherBid === 0`,
+ uuid
+ );
+}, 'browserSignals.highestScoringOtherBid with no other interest groups test.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await joinInterestGroup(test, uuid,
+ {
+ biddingLogicURL: createBiddingScriptURL({ bid: -2 }),
+ name: 'other interest group 1' });
+ await joinInterestGroup(test, uuid,
+ {
+ biddingLogicURL: createBiddingScriptURL({ bid: -1 }),
+ name: 'other interest group 2' });
+ await runReportArgumentValidationTest(
+ test,
+ // reportResultSuccessCondition:
+ `browserSignals.highestScoringOtherBid === 0`,
+ // reportWinSuccessCondition:
+ `browserSignals.highestScoringOtherBid === 0`,
+ uuid
+ );
+}, 'browserSignals.highestScoringOtherBid with other groups that do not bid.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await joinInterestGroup(test, uuid,
+ {
+ biddingLogicURL: createBiddingScriptURL({ bid: 2 }),
+ name: 'other interest group 1' });
+ await joinInterestGroup(test, uuid,
+ {
+ biddingLogicURL: createBiddingScriptURL({ bid: 5 }),
+ name: 'other interest group 2' });
+ await joinInterestGroup(test, uuid,
+ {
+ biddingLogicURL: createBiddingScriptURL({ bid: 2 }),
+ name: 'other interest group 3' });
+ await runReportArgumentValidationTest(
+ test,
+ // reportResultSuccessCondition:
+ `browserSignals.highestScoringOtherBid === 5`,
+ // reportWinSuccessCondition:
+ `browserSignals.highestScoringOtherBid === 5`,
+ uuid
+ );
+}, 'browserSignals.highestScoringOtherBid with other bids.');
+
+subsetTest(promise_test, async test => {
+ await runReportArgumentValidationTest(
+ test,
+ // reportResultSuccessCondition:
+ `browserSignals.interestGroupName === undefined`,
+ // reportWinSuccessCondition:
+ `browserSignals.interestGroupName === ''`
+ );
+}, 'browserSignals.interestGroupName test.');
+
+subsetTest(promise_test, async test => {
+ await runReportArgumentValidationTest(
+ test,
+ // reportResultSuccessCondition:
+ `browserSignals.madeHighestScoringOtherBid === undefined`,
+ // reportWinSuccessCondition:
+ `browserSignals.madeHighestScoringOtherBid === false`
+ );
+}, 'browserSignals.madeHighestScoringOtherBid with no other bids.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await joinInterestGroup(test, uuid,
+ {
+ biddingLogicURL: createBiddingScriptURL({ bid: -1 }),
+ name: 'other interest group 2' });
+ await runReportArgumentValidationTest(
+ test,
+ // reportResultSuccessCondition:
+ `browserSignals.madeHighestScoringOtherBid === undefined`,
+ // reportWinSuccessCondition:
+ `browserSignals.madeHighestScoringOtherBid === false`
+ );
+}, 'browserSignals.madeHighestScoringOtherBid with group that did not bid.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await joinInterestGroup(test, uuid,
+ {
+ biddingLogicURL: createBiddingScriptURL({ bid: 1 }),
+ name: 'other interest group 2' });
+await runReportArgumentValidationTest(
+ test,
+ // reportResultSuccessCondition:
+ `browserSignals.madeHighestScoringOtherBid === undefined`,
+ // reportWinSuccessCondition:
+ `browserSignals.madeHighestScoringOtherBid === true`,
+ uuid
+ );
+}, 'browserSignals.madeHighestScoringOtherBid with other bid.');
diff --git a/testing/web-platform/tests/fledge/tentative/resources/bidding-logic.sub.py b/testing/web-platform/tests/fledge/tentative/resources/bidding-logic.sub.py
new file mode 100644
index 0000000000..707e37f36b
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/resources/bidding-logic.sub.py
@@ -0,0 +1,71 @@
+from pathlib import Path
+
+# General bidding logic script. Depending on query parameters, it can
+# simulate a variety of network errors, and its generateBid() and
+# reportWin() functions can have arbitrary Javascript code injected
+# in them. generateBid() will by default return a bid of 9 for the
+# first ad.
+def main(request, response):
+ error = request.GET.first(b"error", None)
+
+ if error == b"close-connection":
+ # Close connection without writing anything, to simulate a network
+ # error. The write call is needed to avoid writing the default headers.
+ response.writer.write("")
+ response.close_connection = True
+ return
+
+ if error == b"http-error":
+ response.status = (404, b"OK")
+ else:
+ response.status = (200, b"OK")
+
+ if error == b"wrong-content-type":
+ response.headers.set(b"Content-Type", b"application/json")
+ elif error != b"no-content-type":
+ response.headers.set(b"Content-Type", b"application/javascript")
+
+ if error == b"bad-allow-fledge":
+ response.headers.set(b"Ad-Auction-Allowed", b"sometimes")
+ elif error == b"fledge-not-allowed":
+ response.headers.set(b"Ad-Auction-Allowed", b"false")
+ elif error != b"no-allow-fledge":
+ response.headers.set(b"Ad-Auction-Allowed", b"true")
+
+ if error == b"no-body":
+ return b''
+
+ body = (Path(__file__).parent.resolve() / 'worklet-helpers.js').read_text().encode("ASCII")
+ if error != b"no-generateBid":
+ # Use bid query param if present. Otherwise, use a bid of 9.
+ bid = (request.GET.first(b"bid", None) or b"9").decode("ASCII")
+
+ bidCurrency = ""
+ bidCurrencyParam = request.GET.first(b"bidCurrency", None)
+ if bidCurrencyParam != None:
+ bidCurrency = "bidCurrency: '" + bidCurrencyParam.decode("ASCII") + "',"
+
+ allowComponentAuction = ""
+ allowComponentAuctionParam = request.GET.first(b"allowComponentAuction", None)
+ if allowComponentAuctionParam != None:
+ allowComponentAuction = f"allowComponentAuction: {allowComponentAuctionParam.decode('ASCII')},"
+
+ body += f"""
+ function generateBid(interestGroup, auctionSignals, perBuyerSignals,
+ trustedBiddingSignals, browserSignals,
+ directFromSellerSignals) {{
+ {{{{GET[generateBid]}}}};
+ return {{
+ bid: {bid},
+ {bidCurrency}
+ {allowComponentAuction}
+ render: interestGroup.ads[0].renderURL
+ }};
+ }}""".encode()
+ if error != b"no-reportWin":
+ body += b"""
+ function reportWin(auctionSignals, perBuyerSignals, sellerSignals,
+ browserSignals, directFromSellerSignals) {
+ {{GET[reportWin]}};
+ }"""
+ return body
diff --git a/testing/web-platform/tests/fledge/tentative/resources/decision-logic.sub.py b/testing/web-platform/tests/fledge/tentative/resources/decision-logic.sub.py
new file mode 100644
index 0000000000..78d459e3f9
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/resources/decision-logic.sub.py
@@ -0,0 +1,59 @@
+from pathlib import Path
+
+# General decision logic script. Depending on query parameters, it can
+# simulate a variety of network errors, and its scoreAd() and
+# reportResult() functions can have arbitrary Javascript code injected
+# in them. scoreAd() will by default return a desirability score of
+# twice the bid for each ad, as long as the ad URL ends with the uuid.
+def main(request, response):
+ error = request.GET.first(b"error", None)
+
+ if error == b"close-connection":
+ # Close connection without writing anything, to simulate a network
+ # error. The write call is needed to avoid writing the default headers.
+ response.writer.write("")
+ response.close_connection = True
+ return
+
+ if error == b"http-error":
+ response.status = (404, b"OK")
+ else:
+ response.status = (200, b"OK")
+
+ if error == b"wrong-content-type":
+ response.headers.set(b"Content-Type", b"application/json")
+ elif error != b"no-content-type":
+ response.headers.set(b"Content-Type", b"application/javascript")
+
+ if error == b"bad-allow-fledge":
+ response.headers.set(b"Ad-Auction-Allowed", b"sometimes")
+ elif error == b"fledge-not-allowed":
+ response.headers.set(b"Ad-Auction-Allowed", b"false")
+ elif error != b"no-allow-fledge":
+ response.headers.set(b"Ad-Auction-Allowed", b"true")
+
+ if error == b"no-body":
+ return b''
+
+ body = (Path(__file__).parent.resolve() / 'worklet-helpers.js').read_text().encode("ASCII")
+ if error != b"no-scoreAd":
+ body += b"""
+ function scoreAd(adMetadata, bid, auctionConfig, trustedScoringSignals,
+ browserSignals, directFromSellerSignals) {
+ // Don't bid on interest group with the wrong uuid. This is to prevent
+ // left over interest groups from other tests from affecting auction
+ // results.
+ if (!browserSignals.renderUrl.endsWith('uuid={{GET[uuid]}}') &&
+ !browserSignals.renderUrl.includes('uuid={{GET[uuid]}}&')) {
+ return 0;
+ }
+
+ {{GET[scoreAd]}};
+ return {desirability: 2 * bid, allowComponentAuction: true};
+ }"""
+ if error != b"no-reportResult":
+ body += b"""
+ function reportResult(auctionConfig, browserSignals, directFromSellerSignals) {
+ {{GET[reportResult]}};
+ }"""
+ return body
diff --git a/testing/web-platform/tests/fledge/tentative/resources/direct-from-seller-signals.py b/testing/web-platform/tests/fledge/tentative/resources/direct-from-seller-signals.py
new file mode 100644
index 0000000000..14f5ce156e
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/resources/direct-from-seller-signals.py
@@ -0,0 +1,144 @@
+import json
+
+from fledge.tentative.resources import fledge_http_server_util
+
+# Script to return hardcoded "Ad-Auction-Signals" header to test header-based
+# directFromSellerSignals. Requires a "Sec-Ad-Auction-Fetch" header with value
+# of b"?1" in the request, otherwise returns a 400 response.
+#
+# Header "Negative-Test-Option" is used to return some specific hardcoded
+# response for some negative test cases.
+#
+# For all positive test cases, header "Buyer-Origin" is required to be the
+# origin in perBuyerSignals, otherwise return 400 response.
+def main(request, response):
+ if fledge_http_server_util.handle_cors_headers_and_preflight(request, response):
+ return
+
+ # Return 400 if there is no "Sec-Ad-Auction-Fetch" header.
+ if ("Sec-Ad-Auction-Fetch" not in request.headers or
+ request.headers.get("Sec-Ad-Auction-Fetch") != b"?1"):
+ response.status = (400, b"Bad Request")
+ response.headers.set(b"Content-Type", b"text/plain")
+ return "Failed to get Sec-Ad-Auction-Fetch in headers or its value is not \"?1\"."
+
+ # Return 500 to test http error.
+ if ("Negative-Test-Option" in request.headers and
+ request.headers.get("Negative-Test-Option") == b"HTTP Error"):
+ response.status = (500, b"Internal Error")
+ response.headers.set(b"Content-Type", b"text/plain")
+ return "Test http error with 500 response."
+
+ # Return 200 but without "Ad-Auction-Signals" header.
+ if ("Negative-Test-Option" in request.headers and
+ request.headers.get("Negative-Test-Option") == b"No Ad-Auction-Signals Header"):
+ response.status = (200, b"OK")
+ response.headers.set(b"Content-Type", b"text/plain")
+ return "Test 200 response without \"Ad-Auction-Signals\" header."
+
+ # Return 200 but with invalid json in "Ad-Auction-Signals" header.
+ if ("Negative-Test-Option" in request.headers and
+ request.headers.get("Negative-Test-Option") == b"Invalid Json"):
+ response.status = (200, b"OK")
+ response.headers.set("Ad-Auction-Signals", b"[{\"adSlot\": \"adSlot\", \"sellerSignals\": \"sellerSignals\", \"auctionSignals\":}]")
+ response.headers.set(b"Content-Type", b"text/plain")
+ return "Test 200 response with invalid json in \"Ad-Auction-Signals\" header."
+
+ # Return 404 but with valid "Ad-Auction-Signals" header to test network error.
+ if ("Negative-Test-Option" in request.headers and
+ request.headers.get("Negative-Test-Option") == b"Network Error"):
+ response.status = (404, b"Not Found")
+ adAuctionSignals = json.dumps(
+ [{
+ "adSlot": "adSlot",
+ "sellerSignals": "sellerSignals",
+ "auctionSignals": "auctionSignals"
+ }])
+ response.headers.set("Ad-Auction-Signals", adAuctionSignals)
+ response.headers.set(b"Content-Type", b"text/plain")
+ return "Test network error with 400 response code and valid \"Ad-Auction-Signals\" header."
+
+ # For positive test cases, buyer-origin is required, otherwise return 400.
+ if "Buyer-Origin" not in request.headers:
+ response.status = (400, "Bad Request")
+ response.headers.set(b"Content-Type", b"text/plain")
+ return "Failed to get Buyer-Origin in headers."
+
+ response.status = (200, b"OK")
+ buyerOrigin = request.headers.get("Buyer-Origin").decode('utf-8')
+
+ altResponse = request.headers.get("Alternative-Response")
+
+ if altResponse == b"Overwrite adSlot/1":
+ adAuctionSignals = json.dumps(
+ [{
+ "adSlot": "adSlot/1",
+ "sellerSignals": "altSellerSignals/1",
+ }])
+ elif altResponse == b"Overwrite adSlot/1 v2":
+ adAuctionSignals = json.dumps(
+ [{
+ "adSlot": "adSlot/1",
+ "sellerSignals": "altV2SellerSignals/1",
+ }])
+ elif altResponse == b"Two keys with same values":
+ adAuctionSignals = json.dumps(
+ [{
+ "adSlot": "adSlot/1",
+ "sellerSignals": "sameSellerSignals",
+ "auctionSignals": "sameAuctionSignals",
+ "perBuyerSignals": { buyerOrigin: "samePerBuyerSignals" }
+ },
+ {
+ "adSlot": "adSlot/2",
+ "sellerSignals": "sameSellerSignals",
+ "auctionSignals": "sameAuctionSignals",
+ "perBuyerSignals": { buyerOrigin: "samePerBuyerSignals" }
+ }])
+ elif altResponse == b"Duplicate adSlot/1":
+ adAuctionSignals = json.dumps(
+ [{
+ "adSlot": "adSlot/1",
+ "sellerSignals": "firstSellerSignals/1",
+ },
+ {
+ "adSlot": "adSlot/2",
+ "sellerSignals": "nonDupSellerSignals/2",
+ },
+ {
+ "adSlot": "adSlot/1",
+ "sellerSignals": "secondSellerSignals/1",
+ }])
+ else:
+ adAuctionSignals = json.dumps(
+ [{
+ "adSlot": "adSlot/0",
+ },
+ {
+ "adSlot": "adSlot/1",
+ "sellerSignals": "sellerSignals/1",
+ },
+ {
+ "adSlot": "adSlot/2",
+ "auctionSignals": "auctionSignals/2",
+ },
+ {
+ "adSlot": "adSlot/3",
+ "perBuyerSignals": { buyerOrigin: "perBuyerSignals/3" }
+ },
+ {
+ "adSlot": "adSlot/4",
+ "sellerSignals": "sellerSignals/4",
+ "auctionSignals": "auctionSignals/4",
+ "perBuyerSignals": { buyerOrigin: "perBuyerSignals/4" }
+ },
+ {
+ "adSlot": "adSlot/5",
+ "sellerSignals": "sellerSignals/5",
+ "auctionSignals": "auctionSignals/5",
+ "perBuyerSignals": { "mismatchOrigin": "perBuyerSignals/5" }
+ }])
+
+ response.headers.set("Ad-Auction-Signals", adAuctionSignals)
+ response.headers.set(b"Content-Type", b"text/plain")
+ return
diff --git a/testing/web-platform/tests/fledge/tentative/resources/empty.html b/testing/web-platform/tests/fledge/tentative/resources/empty.html
new file mode 100644
index 0000000000..0e76edd65b
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/resources/empty.html
@@ -0,0 +1 @@
+<!DOCTYPE html>
diff --git a/testing/web-platform/tests/fledge/tentative/resources/fenced-frame.sub.py b/testing/web-platform/tests/fledge/tentative/resources/fenced-frame.sub.py
new file mode 100644
index 0000000000..a8f32b6e1e
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/resources/fenced-frame.sub.py
@@ -0,0 +1,26 @@
+# Fenced frame HTML body. Generated by a Python file to avoid having quotes in
+# the injected script escaped, which the test server does to *.html files.
+def main(request, response):
+ response.status = (200, b"OK")
+ response.headers.set(b"Content-Type", b"text/html")
+ response.headers.set(b"Supports-Loading-Mode", b"fenced-frame")
+
+ return """
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <!--- Allow injected scripts to use functions in fledge-util.sub.js --->
+ <base href="..">
+ <script src="/resources/testharness.js"></script>
+ <script src="/common/utils.js"></script>
+ <script src="resources/fledge-util.sub.js"></script>
+ </head>
+ <body>
+ <script>
+ {{GET[script]}}
+ </script>
+ </body>
+ </html>
+ """
+
+
diff --git a/testing/web-platform/tests/fledge/tentative/resources/fledge-util.sub.js b/testing/web-platform/tests/fledge/tentative/resources/fledge-util.sub.js
new file mode 100644
index 0000000000..69573d4998
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/resources/fledge-util.sub.js
@@ -0,0 +1,645 @@
+"use strict;"
+
+const BASE_URL = document.baseURI.substring(0, document.baseURI.lastIndexOf('/') + 1);
+const BASE_PATH = (new URL(BASE_URL)).pathname;
+const RESOURCE_PATH = `${BASE_PATH}resources/`
+
+const DEFAULT_INTEREST_GROUP_NAME = 'default name';
+
+// Unlike other URLs, trusted signals URLs can't have query strings
+// that are set by tests, since FLEDGE controls it entirely, so tests that
+// exercise them use a fixed URL string. Note that FLEDGE adds query
+// params when requesting these URLs, and the python scripts use these
+// to construct the response.
+const TRUSTED_BIDDING_SIGNALS_URL =
+ `${BASE_URL}resources/trusted-bidding-signals.py`;
+const TRUSTED_SCORING_SIGNALS_URL =
+ `${BASE_URL}resources/trusted-scoring-signals.py`;
+
+// Other origins that should all be distinct from the main frame origin
+// that the tests start with.
+const OTHER_ORIGIN1 = 'https://{{hosts[alt][]}}:{{ports[https][0]}}';
+const OTHER_ORIGIN2 = 'https://{{hosts[alt][]}}:{{ports[https][1]}}';
+const OTHER_ORIGIN3 = 'https://{{hosts[][]}}:{{ports[https][1]}}';
+const OTHER_ORIGIN4 = 'https://{{hosts[][www]}}:{{ports[https][0]}}';
+const OTHER_ORIGIN5 = 'https://{{hosts[][www]}}:{{ports[https][1]}}';
+const OTHER_ORIGIN6 = 'https://{{hosts[alt][www]}}:{{ports[https][0]}}';
+const OTHER_ORIGIN7 = 'https://{{hosts[alt][www]}}:{{ports[https][1]}}';
+
+// Creates a URL that will be sent to the URL request tracker script.
+// `uuid` is used to identify the stash shard to use.
+// `dispatch` affects what the tracker script does.
+// `id` can be used to uniquely identify tracked requests. It has no effect
+// on behavior of the script; it only serves to make the URL unique.
+// `id` will always be the last query parameter.
+function createTrackerURL(origin, uuid, dispatch, id = null) {
+ let url = new URL(`${origin}${BASE_PATH}resources/request-tracker.py`);
+ let search = `uuid=${uuid}&dispatch=${dispatch}`;
+ if (id)
+ search += `&id=${id}`;
+ url.search = search;
+ return url.toString();
+}
+
+// Create a URL that when fetches clears tracked URLs. Note that the origin
+// doesn't matter - it will clean up all tracked URLs with the provided uuid,
+// regardless of origin they were fetched from.
+function createCleanupURL(uuid) {
+ return createTrackerURL(window.location.origin, uuid, 'clean_up');
+}
+
+// Create tracked bidder/seller URLs. The only difference is the prefix added
+// to the `id` passed to createTrackerURL. The optional `id` field allows
+// multiple bidder/seller report URLs to be distinguishable from each other.
+// `id` will always be the last query parameter.
+function createBidderReportURL(uuid, id = '1', origin = window.location.origin) {
+ return createTrackerURL(origin, uuid, `track_get`, `bidder_report_${id}`);
+}
+function createSellerReportURL(uuid, id = '1', origin = window.location.origin) {
+ return createTrackerURL(origin, uuid, `track_get`, `seller_report_${id}`);
+}
+
+// Much like above ReportURL methods, except designed for beacons, which
+// are expected to be POSTs.
+function createBidderBeaconURL(uuid, id = '1', origin = window.location.origin) {
+ return createTrackerURL(origin, uuid, `track_post`, `bidder_beacon_${id}`);
+}
+function createSellerBeaconURL(uuid, id = '1', origin = window.location.origin) {
+ return createTrackerURL(origin, uuid, `track_post`, `seller_beacon_${id}`);
+}
+
+function createDirectFromSellerSignalsURL(origin = window.location.origin) {
+ let url = new URL(`${origin}${BASE_PATH}resources/direct-from-seller-signals.py`);
+ return url.toString();
+}
+
+// Generates a UUID and registers a cleanup method with the test fixture to
+// request a URL from the request tracking script that clears all data
+// associated with the generated uuid when requested.
+function generateUuid(test) {
+ let uuid = token();
+ test.add_cleanup(async () => {
+ let response = await fetch(createCleanupURL(uuid),
+ {credentials: 'omit', mode: 'cors'});
+ assert_equals(await response.text(), 'cleanup complete',
+ `Sever state cleanup failed`);
+ });
+ return uuid;
+}
+
+// Helper to fetch "tracked_data" URL to fetch all data recorded by the
+// tracker URL associated with "uuid". Throws on error, including if
+// the retrieved object's errors field is non-empty.
+async function fetchTrackedData(uuid) {
+ let trackedRequestsURL = createTrackerURL(window.location.origin, uuid,
+ 'tracked_data');
+ let response = await fetch(trackedRequestsURL,
+ {credentials: 'omit', mode: 'cors'});
+ let trackedData = await response.json();
+
+ // Fail on fetch error.
+ if (trackedData.error) {
+ throw trackedRequestsURL + ' fetch failed:' + JSON.stringify(trackedData);
+ }
+
+ // Fail on errors reported by the tracker script.
+ if (trackedData.errors.length > 0) {
+ throw 'Errors reported by request-tracker.py:' +
+ JSON.stringify(trackedData.errors);
+ }
+
+ return trackedData;
+}
+
+// Repeatedly requests "tracked_data" URL until exactly the entries in
+// "expectedRequests" have been observed by the request tracker script (in
+// any order, since report URLs are not guaranteed to be sent in any order).
+//
+// Elements of `expectedRequests` should either be URLs, in the case of GET
+// requests, or "<URL>, body: <body>" in the case of POST requests.
+//
+// If any other strings are received from the tracking script, or the tracker
+// script reports an error, fails the test.
+async function waitForObservedRequests(uuid, expectedRequests) {
+ // Sort array for easier comparison, as observed request order does not
+ // matter, and replace UUID to print consistent errors on failure.
+ expectedRequests = expectedRequests.sort().map((url) => url.replace(uuid, '<uuid>'));
+
+ while (true) {
+ let trackedData = await fetchTrackedData(uuid);
+
+ // Clean up "trackedRequests" in same manner as "expectedRequests".
+ let trackedRequests = trackedData.trackedRequests.sort().map(
+ (url) => url.replace(uuid, '<uuid>'));
+
+ // If expected number of requests have been observed, compare with list of
+ // all expected requests and exit.
+ if (trackedRequests.length == expectedRequests.length) {
+ assert_array_equals(trackedRequests, expectedRequests);
+ break;
+ }
+
+ // If fewer than total number of expected requests have been observed,
+ // compare what's been received so far, to have a greater chance to fail
+ // rather than hang on error.
+ for (const trackedRequest of trackedRequests) {
+ assert_in_array(trackedRequest, expectedRequests);
+ }
+ }
+}
+
+// Creates a bidding script with the provided code in the method bodies. The
+// bidding script's generateBid() method will return a bid of 9 for the first
+// ad, after the passed in code in the "generateBid" input argument has been
+// run, unless it returns something or throws.
+//
+// The default reportWin() method is empty.
+function createBiddingScriptURL(params = {}) {
+ let origin = params.origin ? params.origin : new URL(BASE_URL).origin;
+ let url = new URL(`${origin}${RESOURCE_PATH}bidding-logic.sub.py`);
+ // These checks use "==" to ignore null and not provided arguments, while
+ // treating '' as a valid argument.
+ if (params.generateBid != null)
+ url.searchParams.append('generateBid', params.generateBid);
+ if (params.reportWin != null)
+ url.searchParams.append('reportWin', params.reportWin);
+ if (params.error != null)
+ url.searchParams.append('error', params.error);
+ if (params.bid != null)
+ url.searchParams.append('bid', params.bid);
+ if (params.bidCurrency != null)
+ url.searchParams.append('bidCurrency', params.bidCurrency);
+ if (params.allowComponentAuction != null)
+ url.searchParams.append('allowComponentAuction', JSON.stringify(params.allowComponentAuction))
+ return url.toString();
+}
+
+// TODO: Make this return a valid WASM URL.
+function createBiddingWasmHelperURL(params = {}) {
+ let origin = params.origin ? params.origin : new URL(BASE_URL).origin;
+ return `${origin}${RESOURCE_PATH}bidding-wasmlogic.wasm`;
+}
+
+// Creates a decision script with the provided code in the method bodies. The
+// decision script's scoreAd() method will reject ads with renderURLs that
+// don't ends with "uuid", and will return a score equal to the bid, after the
+// passed in code in the "scoreAd" input argument has been run, unless it
+// returns something or throws.
+//
+// The default reportResult() method is empty.
+function createDecisionScriptURL(uuid, params = {}) {
+ let origin = params.origin ? params.origin : new URL(BASE_URL).origin;
+ let url = new URL(`${origin}${RESOURCE_PATH}decision-logic.sub.py`);
+ url.searchParams.append('uuid', uuid);
+ // These checks use "==" to ignore null and not provided arguments, while
+ // treating '' as a valid argument.
+ if (params.scoreAd != null)
+ url.searchParams.append('scoreAd', params.scoreAd);
+ if (params.reportResult != null)
+ url.searchParams.append('reportResult', params.reportResult);
+ if (params.error != null)
+ url.searchParams.append('error', params.error);
+ return url.toString();
+}
+
+// Creates a renderURL for an ad that runs the passed in "script". "uuid" has
+// no effect, beyond making the URL distinct between tests, and being verified
+// by the decision logic script before accepting a bid. "uuid" is expected to
+// be last. "signalsParams" also has no effect, but is used by
+// trusted-scoring-signals.py to affect the response.
+function createRenderURL(uuid, script, signalsParams, origin) {
+ // These checks use "==" to ignore null and not provided arguments, while
+ // treating '' as a valid argument.
+ if (origin == null)
+ origin = new URL(BASE_URL).origin;
+ let url = new URL(`${origin}${RESOURCE_PATH}fenced-frame.sub.py`);
+ if (script != null)
+ url.searchParams.append('script', script);
+ if (signalsParams != null)
+ url.searchParams.append('signalsParams', signalsParams);
+ url.searchParams.append('uuid', uuid);
+ return url.toString();
+}
+
+// Creates an interest group owned by "origin" with a bidding logic URL located
+// on "origin" as well. Uses standard render and report URLs, which are not
+// necessarily on "origin". "interestGroupOverrides" may be used to override any
+// field of the created interest group.
+function createInterestGroupForOrigin(uuid, origin,
+ interestGroupOverrides = {}) {
+ return {
+ owner: origin,
+ name: DEFAULT_INTEREST_GROUP_NAME,
+ biddingLogicURL: createBiddingScriptURL(
+ { origin: origin,
+ reportWin: `sendReportTo('${createBidderReportURL(uuid)}');` }),
+ ads: [{ renderURL: createRenderURL(uuid) }],
+ ...interestGroupOverrides
+ };
+}
+
+// Joins an interest group that, by default, is owned by the current frame's
+// origin, is named DEFAULT_INTEREST_GROUP_NAME, has a bidding script that
+// issues a bid of 9 with a renderURL of "https://not.checked.test/${uuid}",
+// and sends a report to createBidderReportURL(uuid) if it wins. Waits for the
+// join command to complete. Adds cleanup command to `test` to leave the
+// interest group when the test completes.
+//
+// `interestGroupOverrides` may be used to override fields in the joined
+// interest group.
+async function joinInterestGroup(test, uuid, interestGroupOverrides = {},
+ durationSeconds = 60) {
+ let interestGroup = createInterestGroupForOrigin(uuid, window.location.origin,
+ interestGroupOverrides);
+
+ await navigator.joinAdInterestGroup(interestGroup, durationSeconds);
+ test.add_cleanup(
+ async () => {await navigator.leaveAdInterestGroup(interestGroup)});
+}
+
+// Similar to joinInterestGroup, but leaves the interest group instead.
+// Generally does not need to be called manually when using
+// "joinInterestGroup()".
+async function leaveInterestGroup(interestGroupOverrides = {}) {
+ let interestGroup = {
+ owner: window.location.origin,
+ name: DEFAULT_INTEREST_GROUP_NAME,
+ ...interestGroupOverrides
+ };
+
+ await navigator.leaveAdInterestGroup(interestGroup);
+}
+
+// Runs a FLEDGE auction and returns the result. By default, the seller is the
+// current frame's origin, and the only buyer is as well. The seller script
+// rejects bids for URLs that don't contain "uuid" (to avoid running into issues
+// with any interest groups from other tests), and reportResult() sends a report
+// to createSellerReportURL(uuid).
+//
+// `auctionConfigOverrides` may be used to override fields in the auction
+// configuration.
+async function runBasicFledgeAuction(test, uuid, auctionConfigOverrides = {}) {
+ let auctionConfig = {
+ seller: window.location.origin,
+ decisionLogicURL: createDecisionScriptURL(
+ uuid,
+ { reportResult: `sendReportTo('${createSellerReportURL(uuid)}');` }),
+ interestGroupBuyers: [window.location.origin],
+ resolveToConfig: true,
+ ...auctionConfigOverrides
+ };
+ return await navigator.runAdAuction(auctionConfig);
+}
+
+// Checks that await'ed return value of runAdAuction() denotes a successful
+// auction with a winner.
+function expectSuccess(config) {
+ assert_true(config !== null, `Auction unexpectedly had no winner`);
+ assert_true(
+ config instanceof FencedFrameConfig,
+ `Wrong value type returned from auction: ${config.constructor.type}`);
+}
+
+// Checks that await'ed return value of runAdAuction() denotes an auction
+// without a winner (but no fatal error).
+function expectNoWinner(result) {
+ assert_true(result === null, 'Auction unexpectedly had a winner');
+}
+
+// Wrapper around runBasicFledgeAuction() that runs an auction with the specified
+// arguments, expecting the auction to have a winner. Returns the FencedFrameConfig
+// from the auction.
+async function runBasicFledgeTestExpectingWinner(test, uuid, auctionConfigOverrides = {}) {
+ let config = await runBasicFledgeAuction(test, uuid, auctionConfigOverrides);
+ expectSuccess(config);
+ return config;
+}
+
+// Wrapper around runBasicFledgeAuction() that runs an auction with the specified
+// arguments, expecting the auction to have no winner.
+async function runBasicFledgeTestExpectingNoWinner(
+ test, uuid, auctionConfigOverrides = {}) {
+ let result = await runBasicFledgeAuction(test, uuid, auctionConfigOverrides);
+ expectNoWinner(result);
+}
+
+// Creates a fenced frame and applies fencedFrameConfig to it. Also adds a cleanup
+// method to destroy the fenced frame at the end of the current test.
+function createAndNavigateFencedFrame(test, fencedFrameConfig) {
+ let fencedFrame = document.createElement('fencedframe');
+ fencedFrame.mode = 'opaque-ads';
+ fencedFrame.config = fencedFrameConfig;
+ document.body.appendChild(fencedFrame);
+ test.add_cleanup(() => { document.body.removeChild(fencedFrame); });
+}
+
+// Calls runBasicFledgeAuction(), expecting the auction to have a winner.
+// Creates a fenced frame that will be destroyed on completion of "test", and
+// navigates it to the URN URL returned by the auction. Does not wait for the
+// fenced frame to finish loading, since there's no API that can do that.
+async function runBasicFledgeAuctionAndNavigate(test, uuid,
+ auctionConfigOverrides = {}) {
+ let config = await runBasicFledgeTestExpectingWinner(test, uuid,
+ auctionConfigOverrides);
+ createAndNavigateFencedFrame(test, config);
+}
+
+// Joins an interest group and runs an auction, expecting a winner to be
+// returned. "testConfig" can optionally modify the uuid, interest group or
+// auctionConfig.
+async function joinGroupAndRunBasicFledgeTestExpectingWinner(test, testConfig = {}) {
+ const uuid = testConfig.uuid ? testConfig.uuid : generateUuid(test);
+ await joinInterestGroup(test, uuid, testConfig.interestGroupOverrides);
+ await runBasicFledgeTestExpectingWinner(test, uuid, testConfig.auctionConfigOverrides);
+}
+
+// Joins an interest group and runs an auction, expecting no winner to be
+// returned. "testConfig" can optionally modify the uuid, interest group or
+// auctionConfig.
+async function joinGroupAndRunBasicFledgeTestExpectingNoWinner(test, testConfig = {}) {
+ const uuid = testConfig.uuid ? testConfig.uuid : generateUuid(test);
+ await joinInterestGroup(test, uuid, testConfig.interestGroupOverrides);
+ await runBasicFledgeTestExpectingNoWinner(test, uuid, testConfig.auctionConfigOverrides);
+}
+
+// Test helper for report phase of auctions that lets the caller specify the
+// body of reportResult() and reportWin(). Passing in null will cause there
+// to be no reportResult() or reportWin() method.
+//
+// If the "SuccessCondition" fields are non-null and evaluate to false in
+// the corresponding reporting method, the report is sent to an error URL.
+// Otherwise, the corresponding 'reportResult' / 'reportWin' values are run.
+//
+// `codeToInsert` is a JS object that contains the following fields to control
+// the code generated for the auction worklet:
+// scoreAd - function body for scoreAd() seller worklet function
+// reportResultSuccessCondition - Success condition to trigger reportResult()
+// reportResult - function body for reportResult() seller worklet function
+// generateBid - function body for generateBid() buyer worklet function
+// reportWinSuccessCondition - Success condition to trigger reportWin()
+// decisionScriptURLOrigin - Origin of decision script URL
+// reportWin - function body for reportWin() buyer worklet function
+//
+// Additionally the following fields can be added to check for errors during the
+// execution of the corresponding worklets:
+// reportWinSuccessCondition - boolean condition added to reportWin() in the
+// buyer worklet that triggers a sendReportTo() to an 'error' URL if not met.
+// reportResultSuccessCondition - boolean condition added to reportResult() in
+// the seller worklet that triggers a sendReportTo() to an 'error' URL if not
+// met.
+//
+// `renderURLOverride` allows the ad URL of the joined InterestGroup to
+// to be set by the caller.
+//
+// `auctionConfigOverrides` may be used to override fields in the auction
+// configuration.
+//
+// Requesting error report URLs causes waitForObservedRequests() to throw
+// rather than hang.
+async function runReportTest(test, uuid, codeToInsert, expectedReportURLs,
+ renderURLOverride, auctionConfigOverrides) {
+ let scoreAd = codeToInsert.scoreAd;
+ let reportResultSuccessCondition = codeToInsert.reportResultSuccessCondition;
+ let reportResult = codeToInsert.reportResult;
+ let generateBid = codeToInsert.generateBid;
+ let reportWinSuccessCondition = codeToInsert.reportWinSuccessCondition;
+ let reportWin = codeToInsert.reportWin;
+ let decisionScriptURLOrigin = codeToInsert.decisionScriptURLOrigin;
+
+ if (reportResultSuccessCondition) {
+ reportResult = `if (!(${reportResultSuccessCondition})) {
+ sendReportTo('${createSellerReportURL(uuid, 'error')}');
+ return false;
+ }
+ ${reportResult}`;
+ }
+ let decisionScriptURLParams = {};
+
+ if (scoreAd !== undefined) {
+ decisionScriptURLParams.scoreAd = scoreAd;
+ }
+
+ if (reportResult !== null)
+ decisionScriptURLParams.reportResult = reportResult;
+ else
+ decisionScriptURLParams.error = 'no-reportResult';
+
+ if (decisionScriptURLOrigin !== undefined) {
+ decisionScriptURLParams.origin = decisionScriptURLOrigin;
+ }
+
+ if (reportWinSuccessCondition) {
+ reportWin = `if (!(${reportWinSuccessCondition})) {
+ sendReportTo('${createBidderReportURL(uuid, 'error')}');
+ return false;
+ }
+ ${reportWin}`;
+ }
+ let biddingScriptURLParams = {};
+
+ if (generateBid !== undefined) {
+ biddingScriptURLParams.generateBid = generateBid;
+ }
+
+ if (reportWin !== null)
+ biddingScriptURLParams.reportWin = reportWin;
+ else
+ biddingScriptURLParams.error = 'no-reportWin';
+
+ let interestGroupOverrides =
+ { biddingLogicURL: createBiddingScriptURL(biddingScriptURLParams) };
+ if (renderURLOverride)
+ interestGroupOverrides.ads = [{ renderURL: renderURLOverride }]
+
+ await joinInterestGroup(test, uuid, interestGroupOverrides);
+
+ if (auctionConfigOverrides === undefined) {
+ auctionConfigOverrides =
+ { decisionLogicURL: createDecisionScriptURL(uuid, decisionScriptURLParams) };
+ } else if (auctionConfigOverrides.decisionLogicURL === undefined) {
+ auctionConfigOverrides.decisionLogicURL =
+ createDecisionScriptURL(uuid, decisionScriptURLParams);
+ }
+
+ await runBasicFledgeAuctionAndNavigate(test, uuid, auctionConfigOverrides);
+ await waitForObservedRequests(uuid, expectedReportURLs);
+}
+
+// Runs "script" in "child_window" via an eval call. The "child_window" must
+// have been created by calling "createFrame()" below. "param" is passed to the
+// context "script" is run in, so can be used to pass objects that
+// "script" references that can't be serialized to a string, like
+// fencedFrameConfigs.
+async function runInFrame(test, child_window, script, param) {
+ const messageUuid = generateUuid(test);
+ let receivedResponse = {};
+
+ let promise = new Promise(function(resolve, reject) {
+ function WaitForMessage(event) {
+ if (event.data.messageUuid != messageUuid)
+ return;
+ receivedResponse = event.data;
+ if (event.data.result === 'success') {
+ resolve();
+ } else {
+ reject(event.data.result);
+ }
+ }
+ window.addEventListener('message', WaitForMessage);
+ child_window.postMessage(
+ {messageUuid: messageUuid, script: script, param: param}, '*');
+ });
+ await promise;
+ return receivedResponse.returnValue;
+}
+
+// Creates an frame and navigates it to a URL on "origin", and waits for the URL
+// to finish loading by waiting for the frame to send an event. Then returns
+// the frame's Window object. Depending on the value of "is_iframe", the created
+// frame will either be a new iframe, or a new top-level main frame. In the iframe
+// case, its "allow" field will be set to "permissions".
+//
+// Also adds a cleanup callback to "test", which runs all cleanup functions
+// added within the frame and waits for them to complete, and then destroys the
+// iframe or closes the window.
+async function createFrame(test, origin, is_iframe = true, permissions = null) {
+ const frameUuid = generateUuid(test);
+ const frameUrl =
+ `${origin}${RESOURCE_PATH}subordinate-frame.sub.html?uuid=${frameUuid}`;
+ let promise = new Promise(function(resolve, reject) {
+ function WaitForMessage(event) {
+ if (event.data.messageUuid != frameUuid)
+ return;
+ if (event.data.result === 'load complete') {
+ resolve();
+ } else {
+ reject(event.data.result);
+ }
+ }
+ window.addEventListener('message', WaitForMessage);
+ });
+
+ if (is_iframe) {
+ let iframe = document.createElement('iframe');
+ if (permissions)
+ iframe.allow = permissions;
+ iframe.src = frameUrl;
+ document.body.appendChild(iframe);
+
+ test.add_cleanup(async () => {
+ await runInFrame(test, iframe.contentWindow, "await test_instance.do_cleanup();");
+ document.body.removeChild(iframe);
+ });
+
+ await promise;
+ return iframe.contentWindow;
+ }
+
+ let child_window = window.open(frameUrl);
+ test.add_cleanup(async () => {
+ await runInFrame(test, child_window, "await test_instance.do_cleanup();");
+ child_window.close();
+ });
+
+ await promise;
+ return child_window;
+}
+
+// Wrapper around createFrame() that creates an iframe and optionally sets
+// permissions.
+async function createIframe(test, origin, permissions = null) {
+ return await createFrame(test, origin, /*is_iframe=*/true, permissions);
+}
+
+// Wrapper around createFrame() that creates a top-level window.
+async function createTopLevelWindow(test, origin) {
+ return await createFrame(test, origin, /*is_iframe=*/false);
+}
+
+// Joins a cross-origin interest group. Currently does this by joining the
+// interest group in an iframe, though it may switch to using a .well-known
+// fetch to allow the cross-origin join, when support for that is added
+// to these tests, so callers should not assume that's the mechanism in use.
+async function joinCrossOriginInterestGroup(test, uuid, origin, interestGroupOverrides = {}) {
+ let interestGroup = JSON.stringify(
+ createInterestGroupForOrigin(uuid, origin, interestGroupOverrides));
+
+ let iframe = await createIframe(test, origin, 'join-ad-interest-group');
+ await runInFrame(test, iframe,
+ `await joinInterestGroup(test_instance, "${uuid}", ${interestGroup})`);
+}
+
+// Joins an interest group in a top-level window, which has the same origin
+// as the joined interest group.
+async function joinInterestGroupInTopLevelWindow(
+ test, uuid, origin, interestGroupOverrides = {}) {
+ let interestGroup = JSON.stringify(
+ createInterestGroupForOrigin(uuid, origin, interestGroupOverrides));
+
+ let topLeveWindow = await createTopLevelWindow(test, origin);
+ await runInFrame(test, topLeveWindow,
+ `await joinInterestGroup(test_instance, "${uuid}", ${interestGroup})`);
+}
+
+// Fetch directFromSellerSignals from seller and check header
+// 'Ad-Auction-Signals' is hidden from documents.
+async function fetchDirectFromSellerSignals(headers_content, origin) {
+ const response = await fetch(
+ createDirectFromSellerSignalsURL(origin),
+ { adAuctionHeaders: true, headers: headers_content });
+
+ if (!('Negative-Test-Option' in headers_content)) {
+ assert_equals(
+ response.status,
+ 200,
+ 'Failed to fetch directFromSellerSignals: ' + await response.text());
+ }
+ assert_false(
+ response.headers.has('Ad-Auction-Signals'),
+ 'Header "Ad-Auction-Signals" should be hidden from documents.');
+}
+
+// Generate directFromSellerSignals evaluation code for different worklets and
+// pass to `runReportTest()` as `codeToInsert`.
+function directFromSellerSignalsValidatorCode(uuid, expectedSellerSignals,
+ expectedAuctionSignals, expectedPerBuyerSignals) {
+ expectedSellerSignals = JSON.stringify(expectedSellerSignals);
+ expectedAuctionSignals = JSON.stringify(expectedAuctionSignals);
+ expectedPerBuyerSignals = JSON.stringify(expectedPerBuyerSignals);
+
+ return {
+ // Seller worklets
+ scoreAd:
+ `if (directFromSellerSignals === null ||
+ directFromSellerSignals.sellerSignals !== ${expectedSellerSignals} ||
+ directFromSellerSignals.auctionSignals !== ${expectedAuctionSignals} ||
+ Object.keys(directFromSellerSignals).length != 2) {
+ throw 'Failed to get expected directFromSellerSignals in scoreAd(): ' +
+ JSON.stringify(directFromSellerSignals);
+ }`,
+ reportResultSuccessCondition:
+ `directFromSellerSignals !== null &&
+ directFromSellerSignals.sellerSignals === ${expectedSellerSignals} &&
+ directFromSellerSignals.auctionSignals === ${expectedAuctionSignals} &&
+ Object.keys(directFromSellerSignals).length == 2`,
+ reportResult:
+ `sendReportTo("${createSellerReportURL(uuid)}");`,
+
+ // Bidder worklets
+ generateBid:
+ `if (directFromSellerSignals === null ||
+ directFromSellerSignals.perBuyerSignals !== ${expectedPerBuyerSignals} ||
+ directFromSellerSignals.auctionSignals !== ${expectedAuctionSignals} ||
+ Object.keys(directFromSellerSignals).length != 2) {
+ throw 'Failed to get expected directFromSellerSignals in generateBid(): ' +
+ JSON.stringify(directFromSellerSignals);
+ }`,
+ reportWinSuccessCondition:
+ `directFromSellerSignals !== null &&
+ directFromSellerSignals.perBuyerSignals === ${expectedPerBuyerSignals} &&
+ directFromSellerSignals.auctionSignals === ${expectedAuctionSignals} &&
+ Object.keys(directFromSellerSignals).length == 2`,
+ reportWin:
+ `sendReportTo("${createBidderReportURL(uuid)}");`,
+ };
+}
diff --git a/testing/web-platform/tests/fledge/tentative/resources/fledge_http_server_util.py b/testing/web-platform/tests/fledge/tentative/resources/fledge_http_server_util.py
new file mode 100644
index 0000000000..162c93e8b0
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/resources/fledge_http_server_util.py
@@ -0,0 +1,67 @@
+"""Utility functions shared across multiple endpoints."""
+
+
+def headers_to_ascii(headers):
+ """Converts a header map with binary values to one with ASCII values.
+
+ Takes a map of header names to list of values that are all binary strings
+ and returns an otherwise identical map where keys and values have both been
+ converted to ASCII strings.
+
+ Args:
+ headers: header map from binary key to binary value
+
+ Returns header map from ASCII string key to ASCII string value
+ """
+ header_map = {}
+ for pair in headers.items():
+ values = []
+ for value in pair[1]:
+ values.append(value.decode("ASCII"))
+ header_map[pair[0].decode("ASCII")] = values
+ return header_map
+
+
+def handle_cors_headers_and_preflight(request, response):
+ """Applies CORS logic common to many entrypoints.
+
+ Args:
+ request: the wptserve Request that was passed to main
+ response: the wptserve Response that was passed to main
+
+ Returns True if the request is a CORS preflight, which is entirely handled by
+ this function, so that the calling function should immediately return.
+ """
+ # Append CORS headers if needed
+ if b"origin" in request.headers:
+ response.headers.set(b"Access-Control-Allow-Origin",
+ request.headers.get(b"origin"))
+
+ if b"credentials" in request.headers:
+ response.headers.set(b"Access-Control-Allow-Credentials",
+ request.headers.get(b"credentials"))
+
+ # Handle CORS preflight requests.
+ if not request.method == u"OPTIONS":
+ return False
+
+ if not b"Access-Control-Request-Method" in request.headers:
+ response.status = (400, b"Bad Request")
+ response.headers.set(b"Content-Type", b"text/plain")
+ response.content = "Failed to get access-control-request-method in preflight!"
+ return True
+
+ if not b"Access-Control-Request-Headers" in request.headers:
+ response.status = (400, b"Bad Request")
+ response.headers.set(b"Content-Type", b"text/plain")
+ response.content = "Failed to get access-control-request-headers in preflight!"
+ return True
+
+ response.headers.set(b"Access-Control-Allow-Methods",
+ request.headers[b"Access-Control-Request-Method"])
+
+ response.headers.set(b"Access-Control-Allow-Headers",
+ request.headers[b"Access-Control-Request-Headers"])
+
+ response.status = (204, b"No Content")
+ return True
diff --git a/testing/web-platform/tests/fledge/tentative/resources/incrementer.wasm b/testing/web-platform/tests/fledge/tentative/resources/incrementer.wasm
new file mode 100644
index 0000000000..47afcdef2a
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/resources/incrementer.wasm
Binary files differ
diff --git a/testing/web-platform/tests/fledge/tentative/resources/redirect-to-trusted-signals.py b/testing/web-platform/tests/fledge/tentative/resources/redirect-to-trusted-signals.py
new file mode 100644
index 0000000000..7da27cd687
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/resources/redirect-to-trusted-signals.py
@@ -0,0 +1,22 @@
+# Test helper that redirects to "trusted-scoring-signals.py" or
+# "trusted-bidding-signals.py", depending on whether the query params have an
+# "interestGroupNames" entry or not. Query parameters are preserved across the
+# redirect. Used to make sure that trusted signals requests don't follow
+# redirects. Response includes the "Ad-Auction-Allowed" header, which should
+# make no difference; it's present to make sure its absence isn't the reason a
+# redirect was blocked.
+def main(request, response):
+ response.status = (302, "Found")
+ response.headers.set(b"Ad-Auction-Allowed", "true")
+
+ # If there's an "interestGroupNames" query parameter, redirect to bidding
+ # signals. Otherwise, redirect to scoring signals.
+ location = b"trusted-scoring-signals.py?"
+ for param in request.url_parts.query.split("&"):
+ pair = param.split("=", 1)
+ if pair[0] == "interestGroupNames":
+ location = b"trusted-bidding-signals.py?"
+
+ # Append query parameter from current URL to redirect location.
+ location += request.url_parts.query.encode("ASCII")
+ response.headers.set(b"Location", location)
diff --git a/testing/web-platform/tests/fledge/tentative/resources/redirect.py b/testing/web-platform/tests/fledge/tentative/resources/redirect.py
new file mode 100644
index 0000000000..cf0a4718df
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/resources/redirect.py
@@ -0,0 +1,8 @@
+# Test helper that redirects to the location specified by the "location" parameter.
+# For use in testing that redirects are blocked in certain contexts. Response
+# includes the "Ad-Auction-Allowed" header, which should make no difference;
+# it's present to make sure its absence isn't the reason a redirect was blocked.
+def main(request, response):
+ response.status = (302, "Found")
+ response.headers.set(b"Location", request.GET.first(b"location", None))
+ response.headers.set(b"Ad-Auction-Allowed", "true")
diff --git a/testing/web-platform/tests/fledge/tentative/resources/request-tracker.py b/testing/web-platform/tests/fledge/tentative/resources/request-tracker.py
new file mode 100644
index 0000000000..c449d2ab02
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/resources/request-tracker.py
@@ -0,0 +1,113 @@
+import json
+import mimetypes
+import os
+
+from fledge.tentative.resources import fledge_http_server_util
+import wptserve.stash
+from wptserve.utils import isomorphic_decode, isomorphic_encode
+
+
+# Test server that tracks requests it has previously seen, keyed by a token.
+#
+# All requests have a "dispatch" field indicating what to do, and a "uuid"
+# field which should be unique for each test, to avoid tests that fail to
+# clean up after themselves, or that are running concurrently, from interfering
+# with other tests.
+#
+# Each uuid has a stash entry with a dictionary with the following entries:
+# "trackedRequests" is a list of all observed requested URLs with a
+# dispatch of "track_get" or "track_post". POSTS are in the format
+# "<url>, body: <body>".
+# "trackedHeaders" is an object mapping HTTP header names to lists
+# of received HTTP header values for a single request with a
+# dispatch of "track_headers".
+# "errors" is a list of an errors that occurred.
+#
+# A dispatch of "tracked_data" will return all tracked information associated
+# with the uuid, as a JSON string. The "errors" field should be checked by
+# the caller before checking other fields.
+#
+# A dispatch of "clean_up" will delete all information associated with the uuid.
+def main(request, response):
+ # Don't cache responses, since tests expect duplicate requests to always
+ # reach the server.
+ response.headers.set(b"Cache-Control", b"no-store")
+
+ dispatch = request.GET.first(b"dispatch", None)
+ uuid = request.GET.first(b"uuid", None)
+
+ if not uuid or not dispatch:
+ return simple_response(request, response, 404, b"Not found",
+ b"Invalid query parameters")
+
+ stash = request.server.stash
+ with stash.lock:
+ # Take ownership of stashed entry, if any. This removes the entry of the
+ # stash.
+ server_state = stash.take(uuid) or {"trackedRequests": [], "errors": [], "trackedHeaders": None}
+
+ # Clear the entire stash. No work to do, since stash entry was already
+ # removed.
+ if dispatch == b"clean_up":
+ return simple_response(request, response, 200, b"OK",
+ b"cleanup complete")
+
+ # Return the list of entries in the stash. Need to add data back to the
+ # stash first.
+ if dispatch == b"tracked_data":
+ stash.put(uuid, server_state)
+ return simple_response(request, response, 200, b"OK",
+ json.dumps(server_state))
+
+ # Tracks a request that's expected to be a GET.
+ if dispatch == b"track_get":
+ if request.method != "GET":
+ server_state["errors"].append(
+ request.url + " has wrong method: " + request.method)
+ else:
+ server_state["trackedRequests"].append(request.url)
+
+ stash.put(uuid, server_state)
+ return simple_response(request, response, 200, b"OK", b"")
+
+ # Tracks a request that's expected to be a POST.
+ # In addition to the method, check the Content-Type, which is currently
+ # always text/plain. The request body is stored in trackedRequests.
+ if dispatch == b"track_post":
+ contentType = request.headers.get(b"Content-Type", b"missing")
+ if request.method != "POST":
+ server_state["errors"].append(
+ request.url + " has wrong method: " + request.method)
+ elif not contentType.startswith(b"text/plain"):
+ server_state["errors"].append(
+ request.url + " has wrong Content-Type: " +
+ contentType.decode("utf-8"))
+ else:
+ server_state["trackedRequests"].append(
+ request.url + ", body: " + request.body.decode("utf-8"))
+ stash.put(uuid, server_state)
+ return simple_response(request, response, 200, b"OK", b"")
+
+ # Tracks request headers for a single request.
+ if dispatch == b"track_headers":
+ if server_state["trackedHeaders"] != None:
+ server_state["errors"].append("Second track_headers request received.")
+ else:
+ server_state["trackedHeaders"] = fledge_http_server_util.headers_to_ascii(request.headers)
+
+ stash.put(uuid, server_state)
+ return simple_response(request, response, 200, b"OK", b"")
+
+ # Report unrecognized dispatch line.
+ server_state["errors"].append(
+ request.url + " request with unknown dispatch value received: " +
+ dispatch.decode("utf-8"))
+ stash.put(uuid, server_state)
+ return simple_response(request, response, 404, b"Not Found",
+ b"Unrecognized dispatch parameter: " + dispatch)
+
+def simple_response(request, response, status_code, status_message, body,
+ content_type=b"text/plain"):
+ response.status = (status_code, status_message)
+ response.headers.set(b"Content-Type", content_type)
+ return body
diff --git a/testing/web-platform/tests/fledge/tentative/resources/set-cookie.asis b/testing/web-platform/tests/fledge/tentative/resources/set-cookie.asis
new file mode 100644
index 0000000000..96d9f07c57
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/resources/set-cookie.asis
@@ -0,0 +1,3 @@
+HTTP/1.1 200 Ok
+Set-Cookie: cookie=cookie; path=/
+
diff --git a/testing/web-platform/tests/fledge/tentative/resources/subordinate-frame.sub.html b/testing/web-platform/tests/fledge/tentative/resources/subordinate-frame.sub.html
new file mode 100644
index 0000000000..f5b1ef9959
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/resources/subordinate-frame.sub.html
@@ -0,0 +1,89 @@
+<!doctype html>
+<meta charset="utf-8">
+<html>
+<head>
+<!--- Allow injected scripts to use functions in fledge-util.sub.js --->
+<base href="..">
+<script src="/resources/testharness.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/fledge-util.sub.js"></script>
+</head>
+<body>
+<script>
+
+ // This can be used for either iframes or top-level windows.
+ // If there is an opener, this is a top-level window, so
+ // send messages to the opener. Otherwise, this is an iframe,
+ // so send messages to the parent.
+ let message_dest = window.opener;
+ if (!message_dest)
+ message_dest = window.parent;
+
+ // Fake Test class that only supports adding cleanup callbacks,
+ // primarily to leave interest groups once the test is complete.
+ function Test() {
+ this.cleanup_callbacks = [];
+ }
+
+ // Registers a cleanup method with Test.
+ Test.prototype.add_cleanup = function(callback) {
+ this.cleanup_callbacks.push(callback);
+ };
+
+ // Runs all previously registered cleanup methods, waiting for
+ // them all to complete.
+ Test.prototype.do_cleanup = async function() {
+ while (this.cleanup_callbacks.length > 0) {
+ await this.cleanup_callbacks[0]();
+ this.cleanup_callbacks = this.cleanup_callbacks.slice(1);
+ }
+ };
+
+ // Create a bogus test instance that tracks cleanup callbacks. The
+ // main frame managing the test is expected to post a message
+ // to run test_instance.do_cleanup() and wait for it to complete
+ // before destroying the frame.
+ let test_instance = new Test();
+
+ // Register a message event listener that listens for events with data
+ // in the format {messageUuid: <uuid>, script: <script>}, and when such
+ // a message is received, tries to eval the script and then returns a
+ // message in the format:
+ // {messageUuid: <uuid>, result: <result>, returnValue: <returnValue>}
+ //
+ // On success, <result> is "success", while on failure, it's an error
+ // message. <script> is interpreted as a possibly asynchronous function
+ // body. Exceptions are caught and their stringified value is returned
+ // as <result>. <returnValue> is a value returned to the caller of
+ // the function that sent the message. It's up to the received script
+ // to set it, if a return value is needed.
+ //
+ // "messageUuid" serves to allow the listener to make sure the message
+ // is intended for it.
+ window.addEventListener('message', async function(event) {
+ // If not a message for this listener, do nothing.
+ if (!event.data.messageUuid)
+ return;
+ let message = {result: 'unexpected'};
+ try {
+ let param = event.data.param;
+ message = await eval(
+ `(async () => {
+ ${event.data.script};
+ return {result: 'success'};
+ })()`);
+ } catch (e) {
+ message.result = e.toString();
+ }
+ message.messageUuid = event.data.messageUuid;
+
+ message_dest.postMessage(message, '*');
+ });
+
+ // Inform "message_dest" that the frame has finished loading.
+ message_dest.postMessage(
+ {messageUuid: '{{GET[uuid]}}', result: 'load complete'},
+ '*');
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/fledge/tentative/resources/subordinate-frame.sub.html.headers b/testing/web-platform/tests/fledge/tentative/resources/subordinate-frame.sub.html.headers
new file mode 100644
index 0000000000..3e3bda1ec0
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/resources/subordinate-frame.sub.html.headers
@@ -0,0 +1 @@
+Content-Type: text/html; charset=UTF-8 \ No newline at end of file
diff --git a/testing/web-platform/tests/fledge/tentative/resources/trusted-bidding-signals.py b/testing/web-platform/tests/fledge/tentative/resources/trusted-bidding-signals.py
new file mode 100644
index 0000000000..45bede2c45
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/resources/trusted-bidding-signals.py
@@ -0,0 +1,133 @@
+import json
+from urllib.parse import unquote_plus
+
+from fledge.tentative.resources import fledge_http_server_util
+
+
+# Script to generate trusted bidding signals. The response depends on the
+# keys and interestGroupNames - some result in entire response failures, others
+# affect only their own value. Keys are preferentially used over
+# interestGroupName, since keys are composible, but some tests need to cover
+# there being no keys.
+def main(request, response):
+ hostname = None
+ keys = None
+ interestGroupNames = None
+
+ # Manually parse query params. Can't use request.GET because it unescapes as well as splitting,
+ # and commas mean very different things from escaped commas.
+ for param in request.url_parts.query.split("&"):
+ pair = param.split("=", 1)
+ if len(pair) != 2:
+ return fail(response, "Bad query parameter: " + param)
+ # Browsers should escape query params consistently.
+ if "%20" in pair[1]:
+ return fail(response, "Query parameter should escape using '+': " + param)
+
+ # Hostname can't be empty. The empty string can be a key or interest group name, though.
+ if pair[0] == "hostname" and hostname == None and len(pair[1]) > 0:
+ hostname = pair[1]
+ continue
+ if pair[0] == "keys" and keys == None:
+ keys = list(map(unquote_plus, pair[1].split(",")))
+ continue
+ if pair[0] == "interestGroupNames" and interestGroupNames == None:
+ interestGroupNames = list(map(unquote_plus, pair[1].split(",")))
+ continue
+ if pair[0] == "slotSize" or pair[0] == "allSlotsRequestedSizes":
+ continue
+ return fail(response, "Unexpected query parameter: " + param)
+
+ # "interestGroupNames" and "hostname" are mandatory.
+ if not hostname:
+ return fail(response, "hostname missing")
+ if not interestGroupNames:
+ return fail(response, "interestGroupNames missing")
+
+ response.status = (200, b"OK")
+
+ # The JSON representation of this is used as the response body. This does
+ # not currently include a "perInterestGroupData" object.
+ responseBody = {"keys": {}}
+
+ # Set when certain special keys are observed, used in place of the JSON
+ # representation of `responseBody`, when set.
+ body = None
+
+ contentType = "application/json"
+ adAuctionAllowed = "true"
+ dataVersion = None
+ if keys:
+ for key in keys:
+ value = "default value"
+ if key == "close-connection":
+ # Close connection without writing anything, to simulate a
+ # network error. The write call is needed to avoid writing the
+ # default headers.
+ response.writer.write("")
+ response.close_connection = True
+ return
+ elif key.startswith("replace-body:"):
+ # Replace entire response body. Continue to run through other
+ # keys, to allow them to modify request headers.
+ body = key.split(':', 1)[1]
+ elif key.startswith("data-version:"):
+ dataVersion = key.split(':', 1)[1]
+ elif key == "http-error":
+ response.status = (404, b"Not found")
+ elif key == "no-content-type":
+ contentType = None
+ elif key == "wrong-content-type":
+ contentType = 'text/plain'
+ elif key == "bad-ad-auction-allowed":
+ adAuctionAllowed = "sometimes"
+ elif key == "ad-auction-not-allowed":
+ adAuctionAllowed = "false"
+ elif key == "no-ad-auction-allow":
+ adAuctionAllowed = None
+ elif key == "no-value":
+ continue
+ elif key == "wrong-value":
+ responseBody["keys"]["another-value"] = "another-value"
+ continue
+ elif key == "null-value":
+ value = None
+ elif key == "num-value":
+ value = 1
+ elif key == "string-value":
+ value = "1"
+ elif key == "array-value":
+ value = [1, "foo", None]
+ elif key == "object-value":
+ value = {"a":"b", "c":["d"]}
+ elif key == "interest-group-names":
+ value = json.dumps(interestGroupNames)
+ elif key == "hostname":
+ value = request.GET.first(b"hostname", b"not-found").decode("ASCII")
+ elif key == "headers":
+ value = fledge_http_server_util.headers_to_ascii(request.headers)
+ elif key == "slotSize":
+ value = request.GET.first(b"slotSize", b"not-found").decode("ASCII")
+ elif key == "allSlotsRequestedSizes":
+ value = request.GET.first(b"allSlotsRequestedSizes", b"not-found").decode("ASCII")
+ responseBody["keys"][key] = value
+
+ if "data-version" in interestGroupNames:
+ dataVersion = "4"
+
+ if contentType:
+ response.headers.set("Content-Type", contentType)
+ if adAuctionAllowed:
+ response.headers.set("Ad-Auction-Allowed", adAuctionAllowed)
+ if dataVersion:
+ response.headers.set("Data-Version", dataVersion)
+ response.headers.set("Ad-Auction-Bidding-Signals-Format-Version", "2")
+
+ if body != None:
+ return body
+ return json.dumps(responseBody)
+
+def fail(response, body):
+ response.status = (400, "Bad Request")
+ response.headers.set(b"Content-Type", b"text/plain")
+ return body
diff --git a/testing/web-platform/tests/fledge/tentative/resources/trusted-scoring-signals.py b/testing/web-platform/tests/fledge/tentative/resources/trusted-scoring-signals.py
new file mode 100644
index 0000000000..80488a5d6a
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/resources/trusted-scoring-signals.py
@@ -0,0 +1,144 @@
+import json
+from urllib.parse import unquote_plus, urlparse
+
+from fledge.tentative.resources import fledge_http_server_util
+
+
+# Script to generate trusted scoring signals. The responses depends on the
+# query strings in the ads Urls - some result in entire response failures,
+# others affect only their own value. Each renderUrl potentially has a
+# signalsParam, which is a comma-delimited list of instructions that can
+# each affect either the value associated with the renderUrl, or the
+# response as a whole.
+def main(request, response):
+ hostname = None
+ renderUrls = None
+ adComponentRenderURLs = None
+ # List of {type: <render URL type>, urls: <render URL list>} pairs, where <render URL type> is
+ # one of the two render URL dictionary keys used in the response ("renderUrls" or
+ # "adComponentRenderURLs"). May be of length 1 or 2, depending on whether there
+ # are any component URLs.
+ urlLists = []
+
+ # Manually parse query params. Can't use request.GET because it unescapes as well as splitting,
+ # and commas mean very different things from escaped commas.
+ for param in request.url_parts.query.split("&"):
+ pair = param.split("=", 1)
+ if len(pair) != 2:
+ return fail(response, "Bad query parameter: " + param)
+ # Browsers should escape query params consistently.
+ if "%20" in pair[1]:
+ return fail(response, "Query parameter should escape using '+': " + param)
+
+ # Hostname can't be empty. The empty string can be a key or interest group name, though.
+ if pair[0] == "hostname" and hostname == None and len(pair[1]) > 0:
+ hostname = pair[1]
+ continue
+ if pair[0] == "renderUrls" and renderUrls == None:
+ renderUrls = list(map(unquote_plus, pair[1].split(",")))
+ urlLists.append({"type":"renderUrls", "urls":renderUrls})
+ continue
+ if pair[0] == "adComponentRenderUrls" and adComponentRenderURLs == None:
+ adComponentRenderURLs = list(map(unquote_plus, pair[1].split(",")))
+ urlLists.append({"type":"adComponentRenderURLs", "urls":adComponentRenderURLs})
+ continue
+ return fail(response, "Unexpected query parameter: " + param)
+
+ # "hostname" and "renderUrls" are mandatory.
+ if not hostname:
+ return fail(response, "hostname missing")
+ if not renderUrls:
+ return fail(response, "renderUrls missing")
+
+ response.status = (200, b"OK")
+
+ # The JSON representation of this is used as the response body.
+ responseBody = {"renderUrls": {}}
+
+ # Set when certain special keys are observed, used in place of the JSON
+ # representation of `responseBody`, when set.
+ body = None
+
+ contentType = "application/json"
+ adAuctionAllowed = "true"
+ dataVersion = None
+ for urlList in urlLists:
+ for renderUrl in urlList["urls"]:
+ value = "default value"
+ addValue = True
+
+ signalsParams = None
+ for param in urlparse(renderUrl).query.split("&"):
+ pair = param.split("=", 1)
+ if len(pair) != 2:
+ continue
+ if pair[0] == "signalsParams":
+ if signalsParams != None:
+ return fail(response, "renderUrl has multiple signalsParams: " + renderUrl)
+ signalsParams = pair[1]
+ if signalsParams != None:
+ signalsParams = unquote_plus(signalsParams)
+ for signalsParam in signalsParams.split(","):
+ if signalsParam == "close-connection":
+ # Close connection without writing anything, to simulate a
+ # network error. The write call is needed to avoid writing the
+ # default headers.
+ response.writer.write("")
+ response.close_connection = True
+ return
+ elif signalsParam.startswith("replace-body:"):
+ # Replace entire response body. Continue to run through other
+ # renderUrls, to allow them to modify request headers.
+ body = signalsParam.split(':', 1)[1]
+ elif signalsParam.startswith("data-version:"):
+ dataVersion = signalsParam.split(':', 1)[1]
+ elif signalsParam == "http-error":
+ response.status = (404, b"Not found")
+ elif signalsParam == "no-content-type":
+ contentType = None
+ elif signalsParam == "wrong-content-type":
+ contentType = 'text/plain'
+ elif signalsParam == "bad-ad-auction-allowed":
+ adAuctionAllowed = "sometimes"
+ elif signalsParam == "ad-auction-not-allowed":
+ adAuctionAllowed = "false"
+ elif signalsParam == "no-ad-auction-allow":
+ adAuctionAllowed = None
+ elif signalsParam == "wrong-url":
+ renderUrl = "https://wrong-url.test/"
+ elif signalsParam == "no-value":
+ addValue = False
+ elif signalsParam == "null-value":
+ value = None
+ elif signalsParam == "num-value":
+ value = 1
+ elif signalsParam == "string-value":
+ value = "1"
+ elif signalsParam == "array-value":
+ value = [1, "foo", None]
+ elif signalsParam == "object-value":
+ value = {"a":"b", "c":["d"]}
+ elif signalsParam == "hostname":
+ value = request.GET.first(b"hostname", b"not-found").decode("ASCII")
+ elif signalsParam == "headers":
+ value = fledge_http_server_util.headers_to_ascii(request.headers)
+ if addValue:
+ if urlList["type"] not in responseBody:
+ responseBody[urlList["type"]] = {}
+ responseBody[urlList["type"]][renderUrl] = value
+
+ if contentType:
+ response.headers.set("Content-Type", contentType)
+ if adAuctionAllowed:
+ response.headers.set("Ad-Auction-Allowed", adAuctionAllowed)
+ if dataVersion:
+ response.headers.set("Data-Version", dataVersion)
+
+ if body != None:
+ return body
+ return json.dumps(responseBody)
+
+def fail(response, body):
+ response.status = (400, "Bad Request")
+ response.headers.set(b"Content-Type", b"text/plain")
+ return body
diff --git a/testing/web-platform/tests/fledge/tentative/resources/wasm-helper.py b/testing/web-platform/tests/fledge/tentative/resources/wasm-helper.py
new file mode 100644
index 0000000000..a945b4cd5f
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/resources/wasm-helper.py
@@ -0,0 +1,38 @@
+from pathlib import Path
+
+# Returns incrementer.wasm, with appropriate headers. Depending on query
+# parameter, it can simulate a variety of network errors.
+def main(request, response):
+ error = request.GET.first(b"error", None)
+
+ if error == b"close-connection":
+ # Close connection without writing anything, to simulate a network
+ # error. The write call is needed to avoid writing the default headers.
+ response.writer.write("")
+ response.close_connection = True
+ return
+
+ if error == b"http-error":
+ response.status = (404, b"OK")
+ else:
+ response.status = (200, b"OK")
+
+ if error == b"wrong-content-type":
+ response.headers.set(b"Content-Type", b"application/javascript")
+ elif error != b"no-content-type":
+ response.headers.set(b"Content-Type", b"application/wasm")
+
+ if error == b"bad-allow-fledge":
+ response.headers.set(b"Ad-Auction-Allowed", b"sometimes")
+ elif error == b"fledge-not-allowed":
+ response.headers.set(b"Ad-Auction-Allowed", b"false")
+ elif error != b"no-allow-fledge":
+ response.headers.set(b"Ad-Auction-Allowed", b"true")
+
+ if error == b"no-body":
+ return b""
+
+ if error == b"not-wasm":
+ return b"This is not wasm"
+
+ return (Path(__file__).parent.resolve() / "incrementer.wasm").read_bytes()
diff --git a/testing/web-platform/tests/fledge/tentative/resources/worklet-helpers.js b/testing/web-platform/tests/fledge/tentative/resources/worklet-helpers.js
new file mode 100644
index 0000000000..2147a026ae
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/resources/worklet-helpers.js
@@ -0,0 +1,23 @@
+// This file contains helper methods that are appended to the start of bidder
+// and seller worklets.
+
+// Comparison function that checks if two arguments are the same.
+// Not intended for use on anything other than built-in types
+// (Arrays, objects, and primitive types).
+function deepEquals(a, b) {
+ if (typeof a !== typeof b)
+ return false;
+ if (typeof a !== 'object' || a === null || b === null)
+ return a === b;
+
+ let aKeys = Object.keys(a);
+ if (aKeys.length != Object.keys(b).length)
+ return false;
+ for (let key of aKeys) {
+ if (a.hasOwnProperty(key) != b.hasOwnProperty(key) ||
+ !deepEquals(a[key], b[key])) {
+ return false;
+ }
+ }
+ return true;
+}
diff --git a/testing/web-platform/tests/fledge/tentative/round-a-value.https.window.js b/testing/web-platform/tests/fledge/tentative/round-a-value.https.window.js
new file mode 100644
index 0000000000..5bccd4ab07
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/round-a-value.https.window.js
@@ -0,0 +1,161 @@
+// META: script=/resources/testdriver.js
+// META: script=/common/utils.js
+// META: script=resources/fledge-util.sub.js
+// META: timeout=long
+
+"use strict;"
+
+promise_test(async test => {
+ const uuid = generateUuid(test);
+ await runReportTest(
+ test, uuid,
+ { generateBid:
+ `return {'adCost': 1.99,
+ 'bid': 9,
+ 'render': interestGroup.ads[0].renderURL};`,
+ reportWinSuccessCondition:
+ // Possible stochastic rounding results for 1.99
+ `browserSignals.adCost === 1.9921875 || browserSignals.adCost === 1.984375`,
+ reportWin:
+ `sendReportTo('${createBidderReportURL(uuid)}');` },
+ // expectedReportUrls
+ [createBidderReportURL(uuid)]
+ );
+}, 'Check adCost is stochastically rounded with 8 bit mantissa and exponent.');
+
+promise_test(async test => {
+ const uuid = generateUuid(test);
+ await runReportTest(
+ test, uuid,
+ { generateBid:
+ `return {'bid': 1.99,
+ 'render': interestGroup.ads[0].renderURL};`,
+ reportWinSuccessCondition:
+ // Possible stochastic rounding results for 1.99
+ `browserSignals.bid === 1.9921875 || browserSignals.bid === 1.984375`,
+ reportWin:
+ `sendReportTo('${createBidderReportURL(uuid)}');` },
+ // expectedReportUrls
+ [createBidderReportURL(uuid)]
+ );
+}, 'Check bid is stochastically rounded with 8 bit mantissa and exponent.');
+
+promise_test(async test => {
+ const uuid = generateUuid(test);
+ await runReportTest(
+ test, uuid,
+ { scoreAd:
+ `return {desirability: 1.99,
+ allowComponentAuction: false}`,
+ reportResultSuccessCondition:
+ // Possible stochastic rounding results for 1.99
+ `browserSignals.desirability === 1.9921875 || browserSignals.desirability === 1.984375`,
+ reportResult:
+ `sendReportTo('${createBidderReportURL(uuid)}');` },
+ // expectedReportUrls
+ [createBidderReportURL(uuid)]
+ );
+}, 'Check desirability is stochastically rounded with 8 bit mantissa and exponent.');
+
+promise_test(async test => {
+ const uuid = generateUuid(test);
+ await joinInterestGroup(test, uuid,
+ {
+ biddingLogicURL: createBiddingScriptURL({ bid: 1.99 }),
+ name: 'other interest group 1' });
+ await runReportTest(
+ test, uuid,
+ { reportResultSuccessCondition:
+ // Possible stochastic rounding results for 1.99
+ `browserSignals.highestScoringOtherBid === 1.9921875 || browserSignals.highestScoringOtherBid === 1.984375`,
+ reportResult:
+ `sendReportTo('${createBidderReportURL(uuid)}');` },
+ // expectedReportUrls
+ [createBidderReportURL(uuid)]
+ );
+}, 'Check highestScoringOtherBid is stochastically rounded with 8 bit mantissa and exponent.');
+
+promise_test(async test => {
+ const uuid = generateUuid(test);
+ await runReportTest(
+ test, uuid,
+ { generateBid:
+ `return {'adCost': 2,
+ 'bid': 9,
+ 'render': interestGroup.ads[0].renderURL};`,
+ reportWinSuccessCondition:
+ `browserSignals.adCost === 2`,
+ reportWin:
+ `sendReportTo('${createBidderReportURL(uuid)}');` },
+ // expectedReportUrls
+ [createBidderReportURL(uuid)]
+ );
+}, 'Value is ignored as a non-valid floating-point number.');
+
+promise_test(async test => {
+ const uuid = generateUuid(test);
+ await runReportTest(
+ test, uuid,
+ { generateBid:
+ `return {'adCost': 1E-46,
+ 'bid': 9,
+ 'render': interestGroup.ads[0].renderURL};`,
+ reportWinSuccessCondition:
+ `browserSignals.adCost === 0`,
+ reportWin:
+ `sendReportTo('${createBidderReportURL(uuid)}');` },
+ // expectedReportUrls
+ [createBidderReportURL(uuid)]
+ );
+}, 'Value is rounded to 0 if value is greater than 0 and its exponent is less than -128.');
+
+promise_test(async test => {
+ const uuid = generateUuid(test);
+ await runReportTest(
+ test, uuid,
+ { generateBid:
+ `return {'adCost': -1E-46,
+ 'bid': 9,
+ 'render': interestGroup.ads[0].renderURL};`,
+ reportWinSuccessCondition:
+ `browserSignals.adCost === -0`,
+ reportWin:
+ `sendReportTo('${createBidderReportURL(uuid)}');` },
+ // expectedReportUrls
+ [createBidderReportURL(uuid)]
+ );
+}, 'Value is rounded to -0 if value is greater than 0 and its exponent is less than -128.');
+
+promise_test(async test => {
+ const uuid = generateUuid(test);
+ await runReportTest(
+ test, uuid,
+ { generateBid:
+ `return {'adCost': 1E+39,
+ 'bid': 9,
+ 'render': interestGroup.ads[0].renderURL};`,
+ reportWinSuccessCondition:
+ `browserSignals.adCost === Infinity`,
+ reportWin:
+ `sendReportTo('${createBidderReportURL(uuid)}');` },
+ // expectedReportUrls
+ [createBidderReportURL(uuid)]
+ );
+}, 'Value is rounded to Infinity if value is greater than 0 and its exponent is greater than 127.');
+
+promise_test(async test => {
+ const uuid = generateUuid(test);
+ await runReportTest(
+ test, uuid,
+ { generateBid:
+ `return {'adCost': -1E+39,
+ 'bid': 9,
+ 'render': interestGroup.ads[0].renderURL};`,
+ reportWinSuccessCondition:
+ `browserSignals.adCost === -Infinity`,
+ reportWin:
+ `sendReportTo('${createBidderReportURL(uuid)}');` },
+ // expectedReportUrls
+ [createBidderReportURL(uuid)]
+ );
+}, 'Value is rounded to -Infinity if value is less than 0 and its exponent is greater than 127.');
diff --git a/testing/web-platform/tests/fledge/tentative/send-report-to.https.window.js b/testing/web-platform/tests/fledge/tentative/send-report-to.https.window.js
new file mode 100644
index 0000000000..65a2520420
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/send-report-to.https.window.js
@@ -0,0 +1,155 @@
+// META: script=/resources/testdriver.js
+// META: script=/common/utils.js
+// META: script=resources/fledge-util.sub.js
+// META: script=/common/subset-tests.js
+// META: timeout=long
+// META: variant=?1-5
+// META: variant=?6-last
+
+"use strict;"
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await runReportTest(
+ test, uuid,
+ { reportResult:
+ `sendReportTo('${createSellerReportURL(uuid)}');`,
+ reportWinSuccessCondition:
+ 'sellerSignals === null',
+ reportWin:
+ `sendReportTo('${createBidderReportURL(uuid)}');` },
+ // expectedReportUrls:
+ [createSellerReportURL(uuid), createBidderReportURL(uuid)]
+ );
+}, 'Both send reports, seller passes nothing to bidder.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await runReportTest(
+ test, uuid,
+ { reportResult:
+ `sendReportTo('${createSellerReportURL(uuid)}');`,
+ reportWin:
+ '' },
+ // expectedReportUrls:
+ [createSellerReportURL(uuid)]
+ );
+}, 'Only seller sends a report');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await runReportTest(
+ test, uuid,
+ { reportResult:
+ `sendReportTo('${createSellerReportURL(uuid)}');`,
+ reportWin:
+ 'throw new Error("Very serious exception")' },
+ // expectedReportUrls:
+ [createSellerReportURL(uuid)]
+ );
+}, 'Only seller sends a report, bidder throws an exception');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await runReportTest(
+ test, uuid,
+ { reportResult:
+ `sendReportTo('${createSellerReportURL(uuid)}');` },
+ // expectedReportUrls:
+ [createSellerReportURL(uuid)]
+ );
+}, 'Only seller sends a report, bidder has no reportWin() method');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await runReportTest(
+ test, uuid,
+ { reportResult:
+ '',
+ reportWinSuccessCondition:
+ 'sellerSignals === null',
+ reportWin:
+ `sendReportTo('${createBidderReportURL(uuid)}');` },
+ // expectedReportUrls:
+ [createBidderReportURL(uuid)]
+ );
+}, 'Only bidder sends a report');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await runReportTest(
+ test, uuid,
+ { reportResult:
+ 'return "foo";',
+ reportWinSuccessCondition:
+ 'sellerSignals === "foo"',
+ reportWin:
+ `sendReportTo('${createBidderReportURL(uuid)}');` },
+ // expectedReportUrls:
+ [createBidderReportURL(uuid)]
+ );
+}, 'Only bidder sends a report, seller passes a message to bidder');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await runReportTest(
+ test, uuid,
+ { reportResult:
+ 'throw new Error("Very serious exception")',
+ reportWinSuccessCondition:
+ 'sellerSignals === null',
+ reportWin:
+ `sendReportTo('${createBidderReportURL(uuid)}');` },
+ // expectedReportUrls:
+ [createBidderReportURL(uuid)]
+ );
+}, 'Only bidder sends a report, seller throws an exception');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await runReportTest(
+ test, uuid,
+ { reportWinSuccessCondition:
+ 'sellerSignals === null',
+ reportWin:
+ `sendReportTo('${createBidderReportURL(uuid)}');` },
+ // expectedReportUrls:
+ [createBidderReportURL(uuid)]
+ );
+}, 'Only bidder sends a report, seller has no reportResult() method');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await runReportTest(
+ test, uuid,
+ { reportResult:
+ `sendReportTo('${createSellerReportURL(uuid)}');
+ sendReportTo('${createSellerReportURL(uuid)}');
+ return 5;`,
+ reportWinSuccessCondition:
+ 'sellerSignals === null',
+ reportWin:
+ `sendReportTo('${createBidderReportURL(uuid)}');` },
+ // expectedReportUrls:
+ [createBidderReportURL(uuid)]
+ );
+}, 'Seller calls sendReportTo() twice, which throws an exception.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ await runReportTest(
+ test, uuid,
+ { reportResult:
+ `sendReportTo('${createSellerReportURL(uuid)}');`,
+ reportWin:
+ `sendReportTo('${createBidderReportURL(uuid)}');
+ sendReportTo('${createBidderReportURL(uuid)}');` },
+ // expectedReportUrls:
+ [createSellerReportURL(uuid)]
+ );
+ // Seller reports may be sent before bidder reports, since reportWin()
+ // takes output from reportResult() as input. Wait to make sure the
+ // bidder report URL really is not being requested.
+ await new Promise(resolve => test.step_timeout(resolve, 200));
+ await waitForObservedRequests(uuid, [createSellerReportURL(uuid)]);
+}, 'Bidder calls sendReportTo() twice, which throws an exception.');
diff --git a/testing/web-platform/tests/fledge/tentative/tie.https.window.js b/testing/web-platform/tests/fledge/tentative/tie.https.window.js
new file mode 100644
index 0000000000..48d6e95e5c
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/tie.https.window.js
@@ -0,0 +1,125 @@
+// META: script=/resources/testdriver.js
+// META: script=/common/utils.js
+// META: script=resources/fledge-util.sub.js
+// META: timeout=long
+
+"use strict;"
+
+// Runs one auction at a time using `auctionConfigOverrides` until the auction
+// has a winner.
+async function runAuctionsUntilWinner(test, uuid, auctionConfigOverrides) {
+ fencedFrameConfig = null;
+ while (!fencedFrameConfig) {
+ fencedFrameConfig =
+ await runBasicFledgeAuction(test, uuid, auctionConfigOverrides);
+ }
+ return fencedFrameConfig;
+}
+
+// This tests the case of ties. The winner of an auction is normally checked
+// by these tests by checking a report sent when the winner is loaded in a fenced
+// frame. Unfortunately, that requires a lot of navigations, which can be slow.
+//
+// So instead, run a multi-seller auction. The inner auction has two bidders,
+// which both bid, and the seller gives them the same score. For the first
+// auction, the top-level seller just accepts the only bid it sees, and then
+// as usual, we navigate a fenced frame, to learn which bidder won.
+//
+// The for subsequent auctions, the nested component auction is identical,
+// but the top-level auction rejects bids from the bidder that won the
+// first auction. So if we have a winner, we know that the other bidder
+// won the tie. Auctions are run in parallel until this happens.
+//
+// The interest groups use "group-by-origin" execution mode, to potentially
+// allow the auctions run in parallel to complete faster.
+promise_test(async test => {
+ const uuid = generateUuid(test);
+
+ // Use different report URLs for each interest group, to identify
+ // which interest group has won an auction.
+ let reportURLs = [createBidderReportURL(uuid, /*id=*/'1'),
+ createBidderReportURL(uuid, /*id=*/'2')];
+
+ // Use different ad URLs for each auction. These need to be distinct
+ // so that the top-level seller can check the URL to check if the
+ // winning bid from the component auction has already won an
+ // auction.
+ let adURLs = [createRenderURL(uuid),
+ createRenderURL(uuid, /*script=*/';')];
+
+ await Promise.all(
+ [ joinInterestGroup(
+ test, uuid,
+ { name: 'group 1',
+ ads: [{ renderURL: adURLs[0] }],
+ executionMode: 'group-by-origin',
+ biddingLogicURL: createBiddingScriptURL(
+ { allowComponentAuction: true,
+ reportWin: `sendReportTo("${reportURLs[0]}");`})}),
+ joinInterestGroup(
+ test, uuid,
+ { name: 'group 2',
+ ads: [{ renderURL: adURLs[1] }],
+ executionMode: 'group-by-origin',
+ biddingLogicURL: createBiddingScriptURL(
+ { allowComponentAuction: true,
+ reportWin: `sendReportTo("${reportURLs[1]}");`})})
+ ]
+ );
+
+ let componentAuctionConfig = {
+ seller: window.location.origin,
+ decisionLogicURL: createDecisionScriptURL(uuid),
+ interestGroupBuyers: [window.location.origin]
+ };
+
+ let auctionConfigOverrides = {
+ decisionLogicURL: createDecisionScriptURL(uuid),
+ interestGroupBuyers: [],
+ componentAuctions: [componentAuctionConfig]
+ };
+
+ await runBasicFledgeAuctionAndNavigate(test, uuid, auctionConfigOverrides);
+
+ // Waiting for the report URL of the winner should succeed, while waiting for
+ // the one of the loser should throw. Wait for both, see which succeeds, and
+ // set "winningAdURL" to the ad URL of the winner.
+ let winningAdURL = '';
+ try {
+ await waitForObservedRequests(uuid, [reportURLs[0]]);
+ winningAdURL = adURLs[0];
+ } catch (e) {
+ await waitForObservedRequests(uuid, [reportURLs[1]]);
+ winningAdURL = adURLs[1];
+ }
+
+ // Modify `auctionConfigOverrides` to only accept the ad from the interest
+ // group that didn't win the first auction.
+ auctionConfigOverrides.decisionLogicURL =
+ createDecisionScriptURL(
+ uuid,
+ {scoreAd: `if (browserSignals.renderURL == "${winningAdURL}")
+ return 0;`});
+
+ // Add an abort controller, so can cancel extra auctions.
+ let abortController = new AbortController();
+ auctionConfigOverrides.signal = abortController.signal;
+
+ // Run a bunch of auctions in parallel, until one has a winner.
+ let fencedFrameConfig = await Promise.any(
+ [ runAuctionsUntilWinner(test, uuid, auctionConfigOverrides),
+ runAuctionsUntilWinner(test, uuid, auctionConfigOverrides),
+ runAuctionsUntilWinner(test, uuid, auctionConfigOverrides),
+ runAuctionsUntilWinner(test, uuid, auctionConfigOverrides),
+ runAuctionsUntilWinner(test, uuid, auctionConfigOverrides),
+ runAuctionsUntilWinner(test, uuid, auctionConfigOverrides)
+ ]
+ );
+ // Abort the other auctions.
+ abortController.abort('reason');
+
+ // Load the fencedFrameConfig in a fenced frame, and double-check that each
+ // interest group has won once.
+ createAndNavigateFencedFrame(test, fencedFrameConfig);
+ await waitForObservedRequests(uuid, [reportURLs[0], reportURLs[1]]);
+}, 'runAdAuction tie.');
diff --git a/testing/web-platform/tests/fledge/tentative/trusted-bidding-signals.https.window.js b/testing/web-platform/tests/fledge/tentative/trusted-bidding-signals.https.window.js
new file mode 100644
index 0000000000..9799af6ac1
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/trusted-bidding-signals.https.window.js
@@ -0,0 +1,787 @@
+// META: script=/resources/testdriver.js
+// META: script=/common/utils.js
+// META: script=/common/subset-tests.js
+// META: script=resources/fledge-util.sub.js
+// META: timeout=long
+// META: variant=?1-5
+// META: variant=?6-10
+// META: variant=?11-15
+// META: variant=?16-20
+// META: variant=?21-25
+// META: variant=?26-30
+// META: variant=?31-35
+// META: variant=?36-40
+// META: variant=?41-45
+// META: variant=?46-50
+// META: variant=?51-55
+// META: variant=?56-60
+// META: variant=?61-65
+// META: variant=?66-last
+
+"use strict";
+
+// These tests focus on trustedBiddingSignals: Requesting them, handling network
+// errors, handling the keys portion of the response, and passing keys to
+// worklet scripts, and handling the Data-Version header
+//
+// Because of request batching, left over interest groups from
+// other tests may result in tests that request TRUSTED_BIDDING_SIGNALS_URL
+// with certain special keys failing, if interest groups with names other than
+// the default one are not successfully left after previously run tests.
+
+// Helper for trusted bidding signals test. Runs an auction, and fails the
+// test if there's no winner. "generateBidCheck" is an expression that should
+// be true when evaluated in generateBid(). "interestGroupOverrides" is a
+// set of overridden fields added to the default interestGroup when joining it,
+// allowing trusted bidding signals keys and URL to be set, in addition to other
+// fields.
+async function runTrustedBiddingSignalsTest(
+ test, generateBidCheck, interestGroupOverrides = {}, auctionConfigOverrides = {}, uuidOverride = null) {
+ interestGroupOverrides.biddingLogicURL =
+ createBiddingScriptURL({
+ allowComponentAuction: true,
+ generateBid: `if (!(${generateBidCheck})) return false;` });
+ let testConfig = {
+ interestGroupOverrides: interestGroupOverrides,
+ auctionConfigOverrides: auctionConfigOverrides
+ };
+ if (uuidOverride)
+ testConfig.uuid = uuidOverride;
+ await joinGroupAndRunBasicFledgeTestExpectingWinner(test, testConfig);
+}
+
+// Much like runTrustedBiddingSignalsTest, but runs auctions through reporting
+// as well, and evaluates `check` both in generateBid() and reportWin(). Also
+// makes sure browserSignals.dataVersion is undefined in scoreAd() and
+// reportResult().
+async function runTrustedBiddingSignalsDataVersionTest(
+ test, check, interestGroupOverrides = {}) {
+ const uuid = generateUuid(test);
+ interestGroupOverrides.biddingLogicURL =
+ createBiddingScriptURL({
+ generateBid:
+ `if (!(${check})) return false;`,
+ reportWin:
+ `if (!(${check}))
+ sendReportTo('${createBidderReportURL(uuid, 'error')}');
+ else
+ sendReportTo('${createBidderReportURL(uuid)}');` });
+ await joinInterestGroup(test, uuid, interestGroupOverrides);
+
+ const auctionConfigOverrides = {
+ decisionLogicURL: createDecisionScriptURL(
+ uuid,
+ { scoreAd:
+ `if (browserSignals.dataVersion !== undefined)
+ return false;`,
+ reportResult:
+ `if (browserSignals.dataVersion !== undefined)
+ sendReportTo('${createSellerReportURL(uuid, 'error')}')
+ sendReportTo('${createSellerReportURL(uuid)}')`, })
+ }
+ await runBasicFledgeAuctionAndNavigate(test, uuid, auctionConfigOverrides);
+ await waitForObservedRequests(
+ uuid, [createBidderReportURL(uuid), createSellerReportURL(uuid)]);
+}
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsTest(test, 'trustedBiddingSignals === null');
+}, 'No trustedBiddingSignalsKeys or trustedBiddingSignalsURL.');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsTest(
+ test, 'trustedBiddingSignals === null',
+ { trustedBiddingSignalsKeys: ['numValue'] });
+}, 'trustedBiddingSignalsKeys but no trustedBiddingSignalsURL.');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsTest(
+ test, 'trustedBiddingSignals === null',
+ { trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL });
+}, 'trustedBiddingSignalsURL without trustedBiddingSignalsKeys.');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsTest(
+ test, 'trustedBiddingSignals === null',
+ { trustedBiddingSignalsKeys: ['close-connection'],
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL });
+}, 'trustedBiddingSignalsURL closes the connection without sending anything.');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsTest(
+ test, 'trustedBiddingSignals === null',
+ { trustedBiddingSignalsKeys: ['http-error'],
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL });
+}, 'Trusted bidding signals response is HTTP 404 error.');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsTest(
+ test, 'trustedBiddingSignals === null',
+ { trustedBiddingSignalsKeys: ['no-content-type'],
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL });
+}, 'Trusted bidding signals response has no content-type.');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsTest(
+ test, 'trustedBiddingSignals === null',
+ { trustedBiddingSignalsKeys: ['wrong-content-type'],
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL });
+}, 'Trusted bidding signals response has wrong content-type.');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsTest(
+ test, 'trustedBiddingSignals === null',
+ { trustedBiddingSignalsKeys: ['ad-auction-not-allowed'],
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL });
+}, 'Trusted bidding signals response does not allow fledge.');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsTest(
+ test, 'trustedBiddingSignals === null',
+ { trustedBiddingSignalsKeys: ['bad-ad-auction-allowed'],
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL });
+}, 'Trusted bidding signals response has wrong Ad-Auction-Allowed header.');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsTest(
+ test, 'trustedBiddingSignals === null',
+ { trustedBiddingSignalsKeys: ['no-ad-auction-allow'],
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL });
+}, 'Trusted bidding signals response has no Ad-Auction-Allowed header.');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsTest(
+ test, 'trustedBiddingSignals === null',
+ { trustedBiddingSignalsKeys: ['replace-body:'],
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL });
+}, 'Trusted bidding signals response has no body.');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsTest(
+ test, 'trustedBiddingSignals === null',
+ { trustedBiddingSignalsKeys: ['replace-body:Not JSON'],
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL });
+}, 'Trusted bidding signals response is not JSON.');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsTest(
+ test, 'trustedBiddingSignals === null',
+ { trustedBiddingSignalsKeys: ['replace-body:[]'],
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL });
+}, 'Trusted bidding signals response is a JSON array.');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsTest(
+ test, 'trustedBiddingSignals === null',
+ { trustedBiddingSignalsKeys: ['replace-body:{JSON_keys_need_quotes: 1}'],
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL });
+}, 'Trusted bidding signals response in invalid JSON object.');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsTest(
+ test, 'trustedBiddingSignals["replace-body:{}"] === null',
+ { trustedBiddingSignalsKeys: ['replace-body:{}'],
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL });
+}, 'Trusted bidding signals response has no keys object.');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsTest(
+ test, `trustedBiddingSignals['replace-body:{"keys":{}}'] === null`,
+ { trustedBiddingSignalsKeys: ['replace-body:{"keys":{}}'],
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL });
+}, 'Trusted bidding signals response has no keys.');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsTest(
+ test,
+ `trustedBiddingSignals["0"] === null &&
+ trustedBiddingSignals["1"] === null &&
+ trustedBiddingSignals["2"] === null &&
+ trustedBiddingSignals["length"] === null`,
+ { trustedBiddingSignalsKeys:
+ ['replace-body:{"keys":[1,2,3]}', "0", "1", "2", "length"],
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL });
+}, 'Trusted bidding signals response keys is incorrectly an array.');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsTest(
+ test,
+ `trustedBiddingSignals["wrong-value"] === null &&
+ trustedBiddingSignals["another-value"] === undefined`,
+ { trustedBiddingSignalsKeys: ['wrong-value'],
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL });
+}, 'Trusted bidding signals response has key not in trustedBiddingSignalsKeys.');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsTest(
+ test, 'trustedBiddingSignals["null-value"] === null',
+ { trustedBiddingSignalsKeys: ['null-value'],
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL });
+}, 'Trusted bidding signals response has null value for key.');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsTest(
+ test, 'trustedBiddingSignals["num-value"] === 1',
+ { trustedBiddingSignalsKeys: ['num-value'],
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL });
+}, 'Trusted bidding signals response has a number value for key.');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsTest(
+ test, 'trustedBiddingSignals["string-value"] === "1"',
+ { trustedBiddingSignalsKeys: ['string-value'],
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL });
+}, 'Trusted bidding signals response has string value for key.');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsTest(
+ test,
+ `JSON.stringify(trustedBiddingSignals["array-value"]) === '[1,"foo",null]'`,
+ { trustedBiddingSignalsKeys: ['array-value'],
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL });
+}, 'Trusted bidding signals response has array value for key.');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsTest(
+ test,
+ `Object.keys(trustedBiddingSignals["object-value"]).length === 2 &&
+ trustedBiddingSignals["object-value"]["a"] === "b" &&
+ JSON.stringify(trustedBiddingSignals["object-value"]["c"]) === '["d"]'`,
+ { trustedBiddingSignalsKeys: ['object-value'],
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL });
+}, 'Trusted bidding signals response has object value for key.');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsTest(
+ test,
+ 'trustedBiddingSignals[""] === "default value"',
+ { trustedBiddingSignalsKeys: [''],
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL });
+}, 'Trusted bidding signals receives empty string key.');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsTest(
+ test,
+ `Object.keys(trustedBiddingSignals).length === 6 &&
+ trustedBiddingSignals["wrong-value"] === null &&
+ trustedBiddingSignals["null-value"] === null &&
+ trustedBiddingSignals["num-value"] === 1 &&
+ trustedBiddingSignals["string-value"] === "1" &&
+ JSON.stringify(trustedBiddingSignals["array-value"]) === '[1,"foo",null]' &&
+ trustedBiddingSignals[""] === "default value"`,
+ { trustedBiddingSignalsKeys: ['wrong-value', 'null-value', 'num-value',
+ 'string-value', 'array-value', ''],
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL });
+}, 'Trusted bidding signals response has multiple keys.');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsTest(
+ test,
+ 'trustedBiddingSignals["+%20 \x00?,3#&"] === "default value"',
+ { trustedBiddingSignalsKeys: ['+%20 \x00?,3#&'],
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL });
+}, 'Trusted bidding signals receives escaped key.');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsTest(
+ test,
+ 'trustedBiddingSignals["\x00"] === "default value"',
+ { trustedBiddingSignalsKeys: ['\x00'],
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL });
+}, 'Trusted bidding signals receives null key.');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsTest(
+ test,
+ `trustedBiddingSignals["interest-group-names"] === '["${DEFAULT_INTEREST_GROUP_NAME}"]'`,
+ { trustedBiddingSignalsKeys: ['interest-group-names'],
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL });
+}, 'Trusted bidding signals receives interest group name.');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsTest(
+ test,
+ // Interest group names is a JSONified list of JSONified names, so the
+ // null ends up being escaped twice.
+ `trustedBiddingSignals["interest-group-names"] === '["+%20 \\\\u0000?,3#&"]'`,
+ { name: '+%20 \x00?,3#&',
+ trustedBiddingSignalsKeys: ['interest-group-names'],
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL });
+}, 'Trusted bidding signals receives escaped interest group name.');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsTest(
+ test,
+ `trustedBiddingSignals["interest-group-names"] === '[""]'`,
+ { name: '',
+ trustedBiddingSignalsKeys: ['interest-group-names'],
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL });
+}, 'Trusted bidding signals receives empty interest group name.');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsTest(
+ test,
+ `trustedBiddingSignals["hostname"] === "${window.location.hostname}"`,
+ { trustedBiddingSignalsKeys: ['hostname'],
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL });
+}, 'Trusted bidding signals receives hostname field.');
+
+/////////////////////////////////////////////////////////////////////////////
+// Data-Version tests
+/////////////////////////////////////////////////////////////////////////////
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsDataVersionTest(
+ test,
+ 'browserSignals.dataVersion === undefined',
+ { trustedBiddingSignalsKeys: ['num-value'],
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL });
+}, 'Trusted bidding signals response has no Data-Version.');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsDataVersionTest(
+ test,
+ 'browserSignals.dataVersion === 3',
+ { trustedBiddingSignalsKeys: ['data-version:3'],
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL });
+}, 'Trusted bidding signals response has numeric Data-Version.');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsDataVersionTest(
+ test,
+ 'browserSignals.dataVersion === 0',
+ { trustedBiddingSignalsKeys: ['data-version:0'],
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL });
+}, 'Trusted bidding signals response has min Data-Version.');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsDataVersionTest(
+ test,
+ 'browserSignals.dataVersion === 4294967295',
+ { trustedBiddingSignalsKeys: ['data-version:4294967295'],
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL });
+}, 'Trusted bidding signals response has max Data-Version.');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsDataVersionTest(
+ test,
+ 'browserSignals.dataVersion === undefined',
+ { trustedBiddingSignalsKeys: ['data-version:4294967296'],
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL });
+}, 'Trusted bidding signals response has too large Data-Version.');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsDataVersionTest(
+ test,
+ 'browserSignals.dataVersion === undefined',
+ { trustedBiddingSignalsKeys: ['data-version:03'],
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL });
+}, 'Trusted bidding signals response has Data-Version with leading 0.');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsDataVersionTest(
+ test,
+ 'browserSignals.dataVersion === undefined',
+ { trustedBiddingSignalsKeys: ['data-version:-1'],
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL });
+}, 'Trusted bidding signals response has negative Data-Version.');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsDataVersionTest(
+ test,
+ 'browserSignals.dataVersion === undefined',
+ { trustedBiddingSignalsKeys: ['data-version:1.3'],
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL });
+}, 'Trusted bidding signals response has decimal in Data-Version.');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsDataVersionTest(
+ test,
+ 'browserSignals.dataVersion === undefined',
+ { trustedBiddingSignalsKeys: ['data-version:2 2'],
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL });
+}, 'Trusted bidding signals response has space in Data-Version.');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsDataVersionTest(
+ test,
+ 'browserSignals.dataVersion === undefined',
+ { trustedBiddingSignalsKeys: ['data-version:0x4'],
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL });
+}, 'Trusted bidding signals response has hex Data-Version.');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsDataVersionTest(
+ test,
+ 'browserSignals.dataVersion === 4',
+ { name: 'data-version',
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL });
+}, 'Trusted bidding signals response has Data-Version and no trustedBiddingSignalsKeys.');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsDataVersionTest(
+ test,
+ 'browserSignals.dataVersion === undefined',
+ { trustedBiddingSignalsKeys: ['data-version:3', 'replace-body:'],
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL });
+}, 'Trusted bidding signals response with Data-Version and empty body.');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsDataVersionTest(
+ test,
+ 'browserSignals.dataVersion === undefined',
+ { trustedBiddingSignalsKeys: ['data-version:3', 'replace-body:[]'],
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL });
+}, 'Trusted bidding signals response with Data-Version and JSON array body.');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsDataVersionTest(
+ test,
+ 'browserSignals.dataVersion === undefined',
+ { trustedBiddingSignalsKeys: ['data-version:3', 'replace-body:{} {}'],
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL });
+}, 'Trusted bidding signals response with Data-Version and double JSON object body.');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsDataVersionTest(
+ test,
+ 'browserSignals.dataVersion === 3',
+ { trustedBiddingSignalsKeys: ['data-version:3', 'replace-body:{"keys":5}'],
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL });
+}, 'Trusted bidding signals response with Data-Version and invalid keys entry');
+
+/////////////////////////////////////////////////////////////////////////////
+// trustedBiddingSignalsSlotSizeMode tests
+/////////////////////////////////////////////////////////////////////////////
+
+async function runTrustedBiddingSignalsSlotSizeTest(
+ test,
+ expectedSlotSize,
+ expectedAllSlotsRequestedSizes,
+ trustedBiddingSignalsSlotSizeMode = null,
+ auctionConfigOverrides = {},
+ uuidOverride = null) {
+ await runTrustedBiddingSignalsTest(
+ test,
+ `trustedBiddingSignals["slotSize"] ===
+ ${JSON.stringify(expectedSlotSize)} &&
+ trustedBiddingSignals["allSlotsRequestedSizes"] ===
+ ${JSON.stringify(expectedAllSlotsRequestedSizes)}`,
+ { trustedBiddingSignalsKeys: ['slotSize', 'allSlotsRequestedSizes'],
+ trustedBiddingSignalsSlotSizeMode: trustedBiddingSignalsSlotSizeMode,
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL},
+ auctionConfigOverrides,
+ uuidOverride);
+}
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsSlotSizeTest(
+ test,
+ /*expectedSlotSize=*/'not-found',
+ /*expectedAllSlotsRequestedSizes=*/'not-found');
+}, 'Null trustedBiddingSignalsSlotSizeMode, no sizes in AuctionConfig');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsSlotSizeTest(
+ test,
+ /*expectedSlotSize=*/'not-found',
+ /*expectedAllSlotsRequestedSizes=*/'not-found',
+ /*trustedBiddingSignalsSlotSizeMode=*/'not-a-real-mode');
+}, 'Unknown trustedBiddingSignalsSlotSizeMode, no sizes in AuctionConfig');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsSlotSizeTest(
+ test,
+ /*expectedSlotSize=*/'not-found',
+ /*expectedAllSlotsRequestedSizes=*/'not-found',
+ /*trustedBiddingSignalsSlotSizeMode=*/'none');
+}, 'none trustedBiddingSignalsSlotSizeMode, no sizes in AuctionConfig');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsSlotSizeTest(
+ test,
+ /*expectedSlotSize=*/'not-found',
+ /*expectedAllSlotsRequestedSizes=*/'not-found',
+ /*trustedBiddingSignalsSlotSizeMode=*/'slot-size');
+}, 'slot-size trustedBiddingSignalsSlotSizeMode, no sizes in AuctionConfig');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsSlotSizeTest(
+ test,
+ /*expectedSlotSize=*/'not-found',
+ /*expectedAllSlotsRequestedSizes=*/'not-found',
+ /*trustedBiddingSignalsSlotSizeMode=*/'slot-size');
+}, 'all-slots-requested-sizes trustedBiddingSignalsSlotSizeMode, no sizes in AuctionConfig');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsSlotSizeTest(
+ test,
+ /*expectedSlotSize=*/'not-found',
+ /*expectedAllSlotsRequestedSizes=*/'not-found',
+ /*trustedBiddingSignalsSlotSizeMode=*/'none',
+ {requestedSize: {width:'10', height:'20'}});
+}, 'none trustedBiddingSignalsSlotSizeMode, requestedSize in AuctionConfig');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsSlotSizeTest(
+ test,
+ /*expectedSlotSize=*/'not-found',
+ /*expectedAllSlotsRequestedSizes=*/'not-found',
+ /*trustedBiddingSignalsSlotSizeMode=*/null,
+ {requestedSize: {width:'10', height:'20'}});
+}, 'Null trustedBiddingSignalsSlotSizeMode, requestedSize in AuctionConfig');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsSlotSizeTest(
+ test,
+ /*expectedSlotSize=*/'not-found',
+ /*expectedAllSlotsRequestedSizes=*/'not-found',
+ /*trustedBiddingSignalsSlotSizeMode=*/'not-a-real-mode',
+ {requestedSize: {width:'10', height:'20'}});
+}, 'Unknown trustedBiddingSignalsSlotSizeMode, requestedSize in AuctionConfig');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsSlotSizeTest(
+ test,
+ /*expectedSlotSize=*/'10px,20px',
+ /*expectedAllSlotsRequestedSizes=*/'not-found',
+ /*trustedBiddingSignalsSlotSizeMode=*/'slot-size',
+ {requestedSize: {width:'10', height:'20'}});
+}, 'slot-size trustedBiddingSignalsSlotSizeMode, requestedSize in AuctionConfig');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsSlotSizeTest(
+ test,
+ /*expectedSlotSize=*/'not-found',
+ /*expectedAllSlotsRequestedSizes=*/'not-found',
+ /*trustedBiddingSignalsSlotSizeMode=*/'all-slots-requested-sizes',
+ {requestedSize: {width:'10', height:'20'}});
+}, 'all-slots-requested-sizes trustedBiddingSignalsSlotSizeMode, requestedSize in AuctionConfig');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsSlotSizeTest(
+ test,
+ /*expectedSlotSize=*/'not-found',
+ /*expectedAllSlotsRequestedSizes=*/'not-found',
+ /*trustedBiddingSignalsSlotSizeMode=*/'none',
+ {allSlotsRequestedSizes: [{width:10, height:20}]});
+}, 'none trustedBiddingSignalsSlotSizeMode, allSlotsRequestedSizes in AuctionConfig');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsSlotSizeTest(
+ test,
+ /*expectedSlotSize=*/'not-found',
+ /*expectedAllSlotsRequestedSizes=*/'not-found',
+ /*trustedBiddingSignalsSlotSizeMode=*/null,
+ {allSlotsRequestedSizes: [{width:'10', height:'20'}]});
+}, 'Null trustedBiddingSignalsSlotSizeMode, allSlotsRequestedSizes in AuctionConfig');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsSlotSizeTest(
+ test,
+ /*expectedSlotSize=*/'not-found',
+ /*expectedAllSlotsRequestedSizes=*/'not-found',
+ /*trustedBiddingSignalsSlotSizeMode=*/'not-a-real-mode',
+ {allSlotsRequestedSizes: [{width:'10', height:'20'}]});
+}, 'Unknown trustedBiddingSignalsSlotSizeMode, allSlotsRequestedSizes in AuctionConfig');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsSlotSizeTest(
+ test,
+ /*expectedSlotSize=*/'not-found',
+ /*expectedAllSlotsRequestedSizes=*/'not-found',
+ /*trustedBiddingSignalsSlotSizeMode=*/'slot-size',
+ {allSlotsRequestedSizes: [{width:'10', height:'20'}]});
+}, 'slot-size trustedBiddingSignalsSlotSizeMode, allSlotsRequestedSizes in AuctionConfig');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsSlotSizeTest(
+ test,
+ /*expectedSlotSize=*/'not-found',
+ /*expectedAllSlotsRequestedSizes=*/'10px,20px',
+ /*trustedBiddingSignalsSlotSizeMode=*/'all-slots-requested-sizes',
+ {allSlotsRequestedSizes: [{width:'10', height:'20'}]});
+}, 'all-slots-requested-sizes trustedBiddingSignalsSlotSizeMode, allSlotsRequestedSizes in AuctionConfig');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsSlotSizeTest(
+ test,
+ /*expectedSlotSize=*/'10px,20px',
+ /*expectedAllSlotsRequestedSizes=*/'not-found',
+ /*trustedBiddingSignalsSlotSizeMode=*/'slot-size',
+ {requestedSize: {width:'10px', height:'20px'}});
+}, 'slot-size trustedBiddingSignalsSlotSizeMode, explicit pixel units');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsSlotSizeTest(
+ test,
+ /*expectedSlotSize=*/'80sw,12.5sh',
+ /*expectedAllSlotsRequestedSizes=*/'not-found',
+ /*trustedBiddingSignalsSlotSizeMode=*/'slot-size',
+ {requestedSize: {width:'80sw', height:'12.50sh'}});
+}, 'slot-size trustedBiddingSignalsSlotSizeMode, screen size units');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsSlotSizeTest(
+ test,
+ /*expectedSlotSize=*/'80sh,12.5sw',
+ /*expectedAllSlotsRequestedSizes=*/'not-found',
+ /*trustedBiddingSignalsSlotSizeMode=*/'slot-size',
+ {requestedSize: {width:'80sh', height:'12.5sw'}});
+}, 'slot-size trustedBiddingSignalsSlotSizeMode, flipped screen size units');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsSlotSizeTest(
+ test,
+ /*expectedSlotSize=*/'10px,25sh',
+ /*expectedAllSlotsRequestedSizes=*/'not-found',
+ /*trustedBiddingSignalsSlotSizeMode=*/'slot-size',
+ {requestedSize: {width:'10', height:'25sh'}});
+}, 'slot-size trustedBiddingSignalsSlotSizeMode, mixed pixel and screen width units');
+
+subsetTest(promise_test, async test => {
+ await runTrustedBiddingSignalsSlotSizeTest(
+ test,
+ /*expectedSlotSize=*/'not-found',
+ /*expectedAllSlotsRequestedSizes=*/'10px,20px,25sw,20px,22px,80sh',
+ /*trustedBiddingSignalsSlotSizeMode=*/'all-slots-requested-sizes',
+ { allSlotsRequestedSizes: [ {width:'10', height:'20'},
+ {width:'25sw', height:'20px'},
+ {width:'22', height:'80sh'}]});
+}, 'all-slots-requested-sizes trustedBiddingSignalsSlotSizeMode, multiple unit types');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+
+ let group1ReportURL = createBidderReportURL(uuid, /*id=*/'none')
+ let group2ReportURL = createBidderReportURL(uuid, /*id=*/'slot-size')
+ let group3ReportURL = createBidderReportURL(uuid, /*id=*/'all-slots-requested-sizes')
+
+ // The simplest way to make sure interest groups with different modes all receive
+ // the right sizes is to have interest groups that modify their bids based on ad
+ // size sent to the trusted server.
+ await Promise.all(
+ [ joinInterestGroup(
+ test, uuid,
+ { name: 'group 1',
+ trustedBiddingSignalsKeys: ['slotSize', 'allSlotsRequestedSizes'],
+ trustedBiddingSignalsSlotSizeMode: 'none',
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL,
+ biddingLogicURL: createBiddingScriptURL(
+ { generateBid:
+ `if (trustedBiddingSignals["slotSize"] !== "not-found" ||
+ trustedBiddingSignals["allSlotsRequestedSizes"] !== "not-found") {
+ throw "unexpected trustedBiddingSignals";
+ }
+ return {bid: 5, render: interestGroup.ads[0].renderURL};`,
+ reportWin: `sendReportTo("${group1ReportURL}");`})}),
+ joinInterestGroup(
+ test, uuid,
+ { name: 'group 2',
+ trustedBiddingSignalsKeys: ['slotSize', 'allSlotsRequestedSizes'],
+ trustedBiddingSignalsSlotSizeMode: 'slot-size',
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL,
+ biddingLogicURL: createBiddingScriptURL(
+ { generateBid:
+ `if (trustedBiddingSignals["slotSize"] === "not-found" ||
+ trustedBiddingSignals["allSlotsRequestedSizes"] !== "not-found") {
+ throw "unexpected trustedBiddingSignals";
+ }
+ // Group 3 bids using the first digit of the first dimension.
+ return { bid: trustedBiddingSignals["slotSize"].substr(0, 1),
+ render: interestGroup.ads[0].renderURL};`,
+ reportWin: `sendReportTo("${group2ReportURL}");`})}),
+ joinInterestGroup(
+ test, uuid,
+ { name: 'group 3',
+ trustedBiddingSignalsKeys: ['slotSize', 'allSlotsRequestedSizes'],
+ trustedBiddingSignalsSlotSizeMode: 'all-slots-requested-sizes',
+ trustedBiddingSignalsURL: TRUSTED_BIDDING_SIGNALS_URL,
+ biddingLogicURL: createBiddingScriptURL(
+ { generateBid:
+ `if (trustedBiddingSignals["slotSize"] !== "not-found" ||
+ trustedBiddingSignals["allSlotsRequestedSizes"] === "not-found") {
+ throw "unexpected trustedBiddingSignals";
+ }
+ // Group 3 bids using the second digit of the first dimension.
+ return { bid: trustedBiddingSignals["allSlotsRequestedSizes"].substr(1, 1),
+ render: interestGroup.ads[0].renderURL};`,
+ reportWin: `sendReportTo("${group3ReportURL}");`})}),
+ ]
+ );
+
+ let auctionConfigOverrides = {
+ // Disable the default seller reporting, for simplicity.
+ decisionLogicURL: createDecisionScriptURL(uuid, { reportResult: '' }),
+ // Default sizes start with a "11", so groups 2 and 3 will start with a bid
+ // of 1 and lose.
+ requestedSize: {width:'11', height:'20'},
+ allSlotsRequestedSizes: [{width:'11', height:'20'}]
+ };
+
+ // Group 1 wins the first auction.
+ await runBasicFledgeAuctionAndNavigate(test, uuid, auctionConfigOverrides);
+ await waitForObservedRequests(uuid, [group1ReportURL]);
+
+ // Group2 should bid "6" in the second auction, and win it.
+ auctionConfigOverrides.requestedSize = {width:'61', height:'20'};
+ auctionConfigOverrides.allSlotsRequestedSizes = [{width:'61', height:'20'}];
+ await runBasicFledgeAuctionAndNavigate(test, uuid, auctionConfigOverrides);
+ await waitForObservedRequests(uuid, [group1ReportURL, group2ReportURL]);
+
+ // Group3 should bid "7" in the third auction, and win it.
+ auctionConfigOverrides.requestedSize = {width:'67', height:'20'};
+ auctionConfigOverrides.allSlotsRequestedSizes = [{width:'67', height:'20'}];
+ await runBasicFledgeAuctionAndNavigate(test, uuid, auctionConfigOverrides);
+ await waitForObservedRequests(uuid, [group1ReportURL, group2ReportURL, group3ReportURL]);
+}, 'Mixed trustedBiddingSignalsSlotSizeModes in a single auction');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ let componentAuctionConfig = {
+ seller: window.location.origin,
+ decisionLogicURL: createDecisionScriptURL(uuid),
+ interestGroupBuyers: [window.location.origin],
+ requestedSize: {width:'10', height:'20'}
+ };
+
+ let auctionConfigOverrides = {
+ interestGroupBuyers: [],
+ componentAuctions: [componentAuctionConfig],
+ requestedSize: {width:'22', height:'33'}
+ }
+
+ await runTrustedBiddingSignalsSlotSizeTest(
+ test,
+ // Dimensions from the component auction should be used.
+ /*expectedSlotSize=*/'10px,20px',
+ /*expectedAllSlotsRequestedSizes=*/'not-found',
+ /*trustedBiddingSignalsSlotSizeMode=*/'slot-size',
+ auctionConfigOverrides,
+ uuid);
+}, 'slot-size trustedBiddingSignalsSlotSizeMode in a component auction');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ let componentAuctionConfig = {
+ seller: window.location.origin,
+ decisionLogicURL: createDecisionScriptURL(uuid),
+ interestGroupBuyers: [window.location.origin],
+ allSlotsRequestedSizes: [{width:'11', height:'22'}, {width:'12', height:'23'}]
+ };
+
+ let auctionConfigOverrides = {
+ interestGroupBuyers: [],
+ componentAuctions: [componentAuctionConfig],
+ allSlotsRequestedSizes: [{width:'10', height:'20'}]
+ }
+
+ await runTrustedBiddingSignalsSlotSizeTest(
+ test,
+ // Dimensions from the component auction should be used.
+ /*expectedSlotSize=*/'not-found',
+ /*expectedAllSlotsRequestedSizes=*/'11px,22px,12px,23px',
+ /*trustedBiddingSignalsSlotSizeMode=*/'all-slots-requested-sizes',
+ auctionConfigOverrides,
+ uuid);
+}, 'all-slots-requested-sizes trustedBiddingSignalsSlotSizeMode in a component auction');
diff --git a/testing/web-platform/tests/fledge/tentative/trusted-scoring-signals.https.window.js b/testing/web-platform/tests/fledge/tentative/trusted-scoring-signals.https.window.js
new file mode 100644
index 0000000000..67ae3577e4
--- /dev/null
+++ b/testing/web-platform/tests/fledge/tentative/trusted-scoring-signals.https.window.js
@@ -0,0 +1,512 @@
+// META: script=/resources/testdriver.js
+// META: script=/common/utils.js
+// META: script=resources/fledge-util.sub.js
+// META: script=/common/subset-tests.js
+// META: timeout=long
+// META: variant=?1-5
+// META: variant=?6-10
+// META: variant=?11-15
+// META: variant=?16-20
+// META: variant=?21-25
+// META: variant=?26-30
+// META: variant=?31-35
+// META: variant=?36-40
+// META: variant=?41-last
+
+"use strict";
+
+// These tests focus on trustedScoringSignals: Requesting them, handling network
+// errors, handling the renderURLs portion of the response, passing renderURLs
+// to worklet scripts, and handling the Data-Version header.
+
+// Helper for trusted scoring signals tests. Runs an auction with
+// TRUSTED_SCORING_SIGNALS_URL and a single interest group, failing the test
+// if there's no winner. "scoreAdCheck" is an expression that should be true
+// when evaluated in scoreAd(). "renderURL" can be used to control the response
+// given for TRUSTED_SCORING_SIGNALS_URL.
+async function runTrustedScoringSignalsTest(test, uuid, renderURL, scoreAdCheck,
+ additionalInterestGroupOverrides) {
+ const auctionConfigOverrides = {
+ trustedScoringSignalsURL: TRUSTED_SCORING_SIGNALS_URL,
+ decisionLogicURL:
+ createDecisionScriptURL(uuid, {
+ scoreAd: `if (!(${scoreAdCheck})) throw "error";` })};
+ await joinGroupAndRunBasicFledgeTestExpectingWinner(
+ test,
+ {
+ uuid: uuid,
+ interestGroupOverrides: {ads: [{ renderURL: renderURL }],
+ ...additionalInterestGroupOverrides},
+ auctionConfigOverrides: auctionConfigOverrides
+ });
+}
+
+// Much like runTrustedScoringSignalsTest, but runs auctions through reporting
+// as well, and evaluates `check` both in scodeAd() and reportResult(). Also
+// makes sure browserSignals.dataVersion is undefined in generateBid() and
+// reportWin().
+async function runTrustedScoringSignalsDataVersionTest(
+ test, uuid, renderURL, check) {
+ const interestGroupOverrides = {
+ biddingLogicURL:
+ createBiddingScriptURL({
+ generateBid:
+ `if (browserSignals.dataVersion !== undefined)
+ throw "Bad browserSignals.dataVersion"`,
+ reportWin:
+ `if (browserSignals.dataVersion !== undefined)
+ sendReportTo('${createSellerReportURL(uuid, '1-error')}');
+ else
+ sendReportTo('${createSellerReportURL(uuid, '1')}');` }),
+ ads: [{ renderURL: renderURL }]
+ };
+ await joinInterestGroup(test, uuid, interestGroupOverrides);
+
+ const auctionConfigOverrides = {
+ decisionLogicURL: createDecisionScriptURL(
+ uuid,
+ { scoreAd:
+ `if (!(${check})) return false;`,
+ reportResult:
+ `if (!(${check}))
+ sendReportTo('${createSellerReportURL(uuid, '2-error')}')
+ sendReportTo('${createSellerReportURL(uuid, '2')}')`,
+ }),
+ trustedScoringSignalsURL: TRUSTED_SCORING_SIGNALS_URL
+ }
+ await runBasicFledgeAuctionAndNavigate(test, uuid, auctionConfigOverrides);
+ await waitForObservedRequests(
+ uuid, [createSellerReportURL(uuid, '1'), createSellerReportURL(uuid, '2')]);
+}
+
+// Creates a render URL that, when sent to the trusted-scoring-signals.py,
+// results in a trusted scoring signals response with the provided response
+// body.
+function createScoringSignalsRenderUrlWithBody(uuid, responseBody) {
+ return createRenderURL(uuid, /*script=*/null,
+ /*signalsParam=*/`replace-body:${responseBody}`);
+}
+
+/////////////////////////////////////////////////////////////////////////////
+// Tests where no renderURL value is received for the passed in renderURL.
+/////////////////////////////////////////////////////////////////////////////
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ const decisionLogicScriptUrl = createDecisionScriptURL(
+ uuid,
+ { scoreAd: 'if (trustedScoringSignals !== null) throw "error";' });
+ await joinGroupAndRunBasicFledgeTestExpectingWinner(
+ test,
+ { uuid: uuid,
+ auctionConfigOverrides: { decisionLogicURL: decisionLogicScriptUrl }
+ });
+}, 'No trustedScoringSignalsURL.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ const renderURL = createRenderURL(uuid, /*script=*/null, /*signalsParam=*/'close-connection');
+ await runTrustedScoringSignalsTest(
+ test, uuid, renderURL,
+ 'trustedScoringSignals === null');
+}, 'Trusted scoring signals closes the connection without sending anything.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ const renderURL = createRenderURL(uuid, /*script=*/null, /*signalsParam=*/'http-error');
+ await runTrustedScoringSignalsTest(test, uuid, renderURL, 'trustedScoringSignals === null');
+}, 'Trusted scoring signals response is HTTP 404 error.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ const renderURL = createRenderURL(uuid, /*script=*/null, /*signalsParam=*/'no-content-type');
+ await runTrustedScoringSignalsTest(test, uuid, renderURL, 'trustedScoringSignals === null');
+}, 'Trusted scoring signals response has no content-type.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ const renderURL = createRenderURL(uuid, /*script=*/null, /*signalsParam=*/'wrong-content-type');
+ await runTrustedScoringSignalsTest(test, uuid, renderURL, 'trustedScoringSignals === null');
+}, 'Trusted scoring signals response has wrong content-type.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ const renderURL = createRenderURL(uuid, /*script=*/null, /*signalsParam=*/'ad-auction-not-allowed');
+ await runTrustedScoringSignalsTest(test, uuid, renderURL, 'trustedScoringSignals === null');
+}, 'Trusted scoring signals response does not allow FLEDGE.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ const renderURL = createRenderURL(uuid, /*script=*/null, /*signalsParam=*/'bad-ad-auction-allowed');
+ await runTrustedScoringSignalsTest(test, uuid, renderURL, 'trustedScoringSignals === null');
+}, 'Trusted scoring signals response has wrong Ad-Auction-Allowed header.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ const renderURL = createRenderURL(uuid, /*script=*/null, /*signalsParam=*/'no-ad-auction-allow');
+ await runTrustedScoringSignalsTest(test, uuid, renderURL, 'trustedScoringSignals === null');
+}, 'Trusted scoring signals response has no Ad-Auction-Allowed header.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ const renderURL = createScoringSignalsRenderUrlWithBody(
+ uuid, /*responseBody=*/'');
+ await runTrustedScoringSignalsTest(test, uuid, renderURL, 'trustedScoringSignals === null');
+}, 'Trusted scoring signals response has no body.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ const renderURL = createScoringSignalsRenderUrlWithBody(
+ uuid, /*responseBody=*/'Not JSON');
+ await runTrustedScoringSignalsTest(test, uuid, renderURL, 'trustedScoringSignals === null');
+}, 'Trusted scoring signals response is not JSON.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ const renderURL = createScoringSignalsRenderUrlWithBody(
+ uuid, /*responseBody=*/'[]');
+ await runTrustedScoringSignalsTest(test, uuid, renderURL, 'trustedScoringSignals === null');
+}, 'Trusted scoring signals response is a JSON array.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ const renderURL = createScoringSignalsRenderUrlWithBody(
+ uuid, /*responseBody=*/'{JSON_keys_need_quotes: 1}');
+ await runTrustedScoringSignalsTest(test, uuid, renderURL, 'trustedScoringSignals === null');
+}, 'Trusted scoring signals response is invalid JSON object.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ const renderURL = createScoringSignalsRenderUrlWithBody(
+ uuid, /*responseBody=*/'{}');
+ await runTrustedScoringSignalsTest(
+ test, uuid, renderURL,
+ `trustedScoringSignals.renderURL["${renderURL}"] === null`);
+}, 'Trusted scoring signals response has no renderURL object.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ const renderURL = createRenderURL(uuid, /*script=*/null, /*signalsParam=*/'no-value');
+ await runTrustedScoringSignalsTest(
+ test, uuid, renderURL,
+ `trustedScoringSignals.renderURL["${renderURL}"] === null`);
+}, 'Trusted scoring signals response has no renderURLs.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ const renderURL = createRenderURL(uuid, /*script=*/null, /*signalsParam=*/'wrong-url');
+ await runTrustedScoringSignalsTest(
+ test, uuid, renderURL,
+ `trustedScoringSignals.renderURL["${renderURL}"] === null &&
+ Object.keys(trustedScoringSignals.renderURL).length === 1`);
+}, 'Trusted scoring signals response has renderURL not in response.');
+
+/////////////////////////////////////////////////////////////////////////////
+// Tests where renderURL value is received for the passed in renderURL.
+/////////////////////////////////////////////////////////////////////////////
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ const renderURL = createRenderURL(uuid, /*script=*/null, /*signalsParam=*/'null-value');
+ await runTrustedScoringSignalsTest(
+ test, uuid, renderURL,
+ `trustedScoringSignals.renderURL["${renderURL}"] === null`);
+}, 'Trusted scoring signals response has null value for renderURL.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ const renderURL = createRenderURL(uuid, /*script=*/null, /*signalsParam=*/'num-value');
+ await runTrustedScoringSignalsTest(
+ test, uuid, renderURL,
+ `trustedScoringSignals.renderURL["${renderURL}"] === 1`);
+}, 'Trusted scoring signals response has a number value for renderURL.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ const renderURL = createRenderURL(uuid, /*script=*/null,
+ /*signalsParam=*/'string-value');
+ await runTrustedScoringSignalsTest(
+ test, uuid, renderURL,
+ `trustedScoringSignals.renderURL["${renderURL}"] === "1"`);
+}, 'Trusted scoring signals response has a string value for renderURL.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ const renderURL = createRenderURL(uuid, /*script=*/null, /*signalsParam=*/'array-value');
+ await runTrustedScoringSignalsTest(
+ test, uuid, renderURL,
+ `JSON.stringify(trustedScoringSignals.renderURL["${renderURL}"]) === '[1,"foo",null]'`);
+}, 'Trusted scoring signals response has an array value for renderURL.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ const renderURL = createRenderURL(uuid, /*script=*/null, /*signalsParam=*/'object-value');
+ await runTrustedScoringSignalsTest(
+ test, uuid, renderURL,
+ `Object.keys(trustedScoringSignals.renderURL["${renderURL}"]).length === 2 &&
+ trustedScoringSignals.renderURL["${renderURL}"]["a"] === "b" &&
+ JSON.stringify(trustedScoringSignals.renderURL["${renderURL}"]["c"]) === '["d"]'`);
+}, 'Trusted scoring signals response has an object value for renderURL.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ const renderURL = createRenderURL(uuid, /*script=*/null, /*signalsParam=*/'+%20 \x00?,3#&');
+ await runTrustedScoringSignalsTest(
+ test, uuid, renderURL,
+ `trustedScoringSignals.renderURL["${renderURL}"] === "default value"`);
+}, 'Trusted scoring signals with escaped renderURL.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ const renderURL = createRenderURL(uuid, /*script=*/null, /*signalsParam=*/'hostname');
+ await runTrustedScoringSignalsTest(
+ test, uuid, renderURL,
+ `trustedScoringSignals.renderURL["${renderURL}"] === "${window.location.hostname}"`);
+}, 'Trusted scoring signals receives hostname field.');
+
+// Joins two interest groups and makes sure the scoring signals for one are never leaked
+// to the seller script when scoring the other.
+//
+// There's no guarantee in this test that a single request to the server will be made with
+// render URLs from two different IGs, though that's the case this is trying to test -
+// browsers are not required to support batching, and even if they do, joining any two
+// particular requests may be racy.
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ const renderURL1 = createRenderURL(uuid, /*script=*/null, /*signalsParam=*/'num-value');
+ const renderURL2 = createRenderURL(uuid, /*script=*/null, /*signalsParam=*/'string-value');
+ await joinInterestGroup(test, uuid, { ads: [{ renderURL: renderURL1 }], name: '1' });
+ await joinInterestGroup(test, uuid, { ads: [{ renderURL: renderURL2 }], name: '2' });
+ let auctionConfigOverrides = { trustedScoringSignalsURL: TRUSTED_SCORING_SIGNALS_URL };
+
+ // scoreAd() only accepts the first IG's bid, validating its trustedScoringSignals.
+ auctionConfigOverrides.decisionLogicURL =
+ createDecisionScriptURL(uuid, {
+ scoreAd: `if (browserSignals.renderURL === "${renderURL1}" &&
+ trustedScoringSignals.renderURL["${renderURL1}"] !== 1 ||
+ trustedScoringSignals.renderURL["${renderURL2}"] !== undefined)
+ return;` });
+ let config = await runBasicFledgeAuction(
+ test, uuid, auctionConfigOverrides);
+ assert_true(config instanceof FencedFrameConfig,
+ `Wrong value type returned from first auction: ${config.constructor.type}`);
+
+ // scoreAd() only accepts the second IG's bid, validating its trustedScoringSignals.
+ auctionConfigOverrides.decisionLogicURL =
+ createDecisionScriptURL(uuid, {
+ scoreAd: `if (browserSignals.renderURL === "${renderURL2}" &&
+ trustedScoringSignals.renderURL["${renderURL1}"] !== undefined ||
+ trustedScoringSignals.renderURL["${renderURL2}"] !== '1')
+ return;` });
+ config = await runBasicFledgeAuction(
+ test, uuid, auctionConfigOverrides);
+ assert_true(config instanceof FencedFrameConfig,
+ `Wrong value type returned from second auction: ${config.constructor.type}`);
+}, 'Trusted scoring signals multiple renderURLs.');
+
+/////////////////////////////////////////////////////////////////////////////
+// Data-Version tests
+/////////////////////////////////////////////////////////////////////////////
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ const renderURL = createRenderURL(uuid);
+ await runTrustedScoringSignalsDataVersionTest(
+ test, uuid, renderURL,
+ 'browserSignals.dataVersion === undefined');
+}, 'Trusted scoring signals response has no Data-Version.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ const renderURL = createRenderURL(uuid, /*script=*/null, 'data-version:3');
+ await runTrustedScoringSignalsDataVersionTest(
+ test, uuid, renderURL,
+ 'browserSignals.dataVersion === 3');
+}, 'Trusted scoring signals response has valid Data-Version.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ const renderURL = createRenderURL(uuid, /*script=*/null, 'data-version:0');
+ await runTrustedScoringSignalsDataVersionTest(
+ test, uuid, renderURL,
+ 'browserSignals.dataVersion === 0');
+}, 'Trusted scoring signals response has min Data-Version.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ const renderURL = createRenderURL(uuid, /*script=*/null, 'data-version:4294967295');
+ await runTrustedScoringSignalsDataVersionTest(
+ test, uuid, renderURL,
+ 'browserSignals.dataVersion === 4294967295');
+}, 'Trusted scoring signals response has max Data-Version.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ const renderURL = createRenderURL(uuid, /*script=*/null, 'data-version:4294967296');
+ await runTrustedScoringSignalsDataVersionTest(
+ test, uuid, renderURL,
+ 'browserSignals.dataVersion === undefined');
+}, 'Trusted scoring signals response has too large Data-Version.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ const renderURL = createRenderURL(uuid, /*script=*/null, 'data-version:03');
+ await runTrustedScoringSignalsDataVersionTest(
+ test, uuid, renderURL,
+ 'browserSignals.dataVersion === undefined');
+}, 'Trusted scoring signals response has data-version with leading 0.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ const renderURL = createRenderURL(uuid, /*script=*/null, 'data-version:-1');
+ await runTrustedScoringSignalsDataVersionTest(
+ test, uuid, renderURL,
+ 'browserSignals.dataVersion === undefined');
+}, 'Trusted scoring signals response has negative Data-Version.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ const renderURL = createRenderURL(uuid, /*script=*/null, 'data-version:1.3');
+ await runTrustedScoringSignalsDataVersionTest(
+ test, uuid, renderURL,
+ 'browserSignals.dataVersion === undefined');
+}, 'Trusted scoring signals response has decimal in Data-Version.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ const renderURL = createRenderURL(uuid, /*script=*/null, 'data-version:2 2');
+ await runTrustedScoringSignalsDataVersionTest(
+ test, uuid, renderURL,
+ 'browserSignals.dataVersion === undefined');
+}, 'Trusted scoring signals response has space in Data-Version.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ const renderURL = createRenderURL(uuid, /*script=*/null, 'data-version:0x4');
+ await runTrustedScoringSignalsDataVersionTest(
+ test, uuid, renderURL,
+ 'browserSignals.dataVersion === undefined');
+}, 'Trusted scoring signals response has hex Data-Version.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ const renderURL = createRenderURL(uuid, /*script=*/null, 'data-version:3,replace-body:');
+ await runTrustedScoringSignalsDataVersionTest(
+ test, uuid, renderURL,
+ 'browserSignals.dataVersion === undefined');
+}, 'Trusted scoring signals response has data-version and empty body.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ const renderURL = createRenderURL(uuid, /*script=*/null, 'data-version:3,replace-body:[]');
+ await runTrustedScoringSignalsDataVersionTest(
+ test, uuid, renderURL,
+ 'browserSignals.dataVersion === undefined');
+}, 'Trusted scoring signals response has data-version and JSON array body.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ const renderURL = createRenderURL(uuid, /*script=*/null, 'data-version:3,replace-body:{} {}');
+ await runTrustedScoringSignalsDataVersionTest(
+ test, uuid, renderURL,
+ 'browserSignals.dataVersion === undefined');
+}, 'Trusted scoring signals response has data-version and double JSON object body.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ const renderURL = createRenderURL(uuid, /*script=*/null, 'data-version:3,replace-body:{}');
+ await runTrustedScoringSignalsDataVersionTest(
+ test, uuid, renderURL,
+ 'browserSignals.dataVersion === 3');
+}, 'Trusted scoring signals response has data-version and no renderURLs.');
+
+/////////////////////////////////////////////////////////////////////////////
+// Trusted scoring signals + component ad tests.
+/////////////////////////////////////////////////////////////////////////////
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ const renderURL = createRenderURL(uuid, /*script=*/null, /*signalsParam=*/'close-connection');
+ const componentURL = createRenderURL(uuid, /*script=*/null);
+ await runTrustedScoringSignalsTest(
+ test, uuid, renderURL,
+ `trustedScoringSignals === null`,
+ {adComponents: [{ renderURL: componentURL }],
+ biddingLogicURL: createBiddingScriptURL({
+ generateBid: `return {bid: 1,
+ render: interestGroup.ads[0].renderURL,
+ adComponents: [interestGroup.adComponents[0].renderURL]};`})
+ });
+}, 'Component ads trusted scoring signals, server closes the connection without sending anything.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ const renderURL = createRenderURL(uuid, /*script=*/null, /*signalsParam=*/'num-value');
+ // This should not be sent. If it is, it will take precedence over the "num-value" parameter
+ // from "renderURL", resulting in the "renderURL" having a null "trustedScoringSignals" value.
+ const componentURL = createScoringSignalsRenderUrlWithBody(
+ uuid, /*responseBody=*/'{}');
+ await runTrustedScoringSignalsTest(
+ test, uuid, renderURL,
+ `Object.keys(trustedScoringSignals.renderURL).length === 1 &&
+ trustedScoringSignals.renderURL["${renderURL}"] === 1 &&
+ trustedScoringSignals.adComponentRenderURLs === undefined`,
+ { adComponents: [{ renderURL: componentURL }] });
+}, 'Trusted scoring signals request without component ads in bid.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ const renderURL = createScoringSignalsRenderUrlWithBody(
+ uuid, /*responseBody=*/'{}');
+ const componentURL = createRenderURL(uuid, /*script=*/null);
+ await runTrustedScoringSignalsTest(
+ test, uuid, renderURL,
+ `Object.keys(trustedScoringSignals.renderURL).length === 1 &&
+ trustedScoringSignals.renderURL["${renderURL}"] === null &&
+ Object.keys(trustedScoringSignals.adComponentRenderURLs).length === 1 &&
+ trustedScoringSignals.adComponentRenderURLs["${componentURL}"] === null`,
+ {adComponents: [{ renderURL: componentURL }],
+ biddingLogicURL: createBiddingScriptURL({
+ generateBid: `return {bid: 1,
+ render: interestGroup.ads[0].renderURL,
+ adComponents: [interestGroup.adComponents[0].renderURL]};`})
+ });
+}, 'Component ads trusted scoring signals trusted scoring signals response is empty JSON object.');
+
+subsetTest(promise_test, async test => {
+ const uuid = generateUuid(test);
+ const renderURL = createRenderURL(uuid, /*script=*/null, /*signalsParam=*/'hostname');
+ const componentURL1 = createRenderURL(uuid, /*script=*/null, /*signalsParam=*/'null-value');
+ const componentURL2 = createRenderURL(uuid, /*script=*/null, /*signalsParam=*/'num-value');
+ const componentURL3 = createRenderURL(uuid, /*script=*/null, /*signalsParam=*/'string-value');
+ const componentURL4 = createRenderURL(uuid, /*script=*/null, /*signalsParam=*/'array-value');
+ const componentURL5 = createRenderURL(uuid, /*script=*/null, /*signalsParam=*/'object-value');
+ const componentURL6 = createRenderURL(uuid, /*script=*/null, /*signalsParam=*/'wrong-url');
+
+ await runTrustedScoringSignalsTest(
+ test, uuid, renderURL,
+ `Object.keys(trustedScoringSignals.renderURL).length === 1 &&
+ trustedScoringSignals.renderURL["${renderURL}"] === "${window.location.hostname}" &&
+
+ Object.keys(trustedScoringSignals.adComponentRenderURLs).length === 6 &&
+ trustedScoringSignals.adComponentRenderURLs["${componentURL1}"] === null &&
+ trustedScoringSignals.adComponentRenderURLs["${componentURL2}"] === 1 &&
+ trustedScoringSignals.adComponentRenderURLs["${componentURL3}"] === "1" &&
+ JSON.stringify(trustedScoringSignals.adComponentRenderURLs["${componentURL4}"]) === '[1,"foo",null]' &&
+ Object.keys(trustedScoringSignals.adComponentRenderURLs["${componentURL5}"]).length === 2 &&
+ trustedScoringSignals.adComponentRenderURLs["${componentURL5}"]["a"] === "b" &&
+ JSON.stringify(trustedScoringSignals.adComponentRenderURLs["${componentURL5}"]["c"]) === '["d"]' &&
+ trustedScoringSignals.adComponentRenderURLs["${componentURL6}"] === null`,
+
+ {adComponents: [{ renderURL: componentURL1 }, { renderURL: componentURL2 },
+ { renderURL: componentURL3 }, { renderURL: componentURL4 },
+ { renderURL: componentURL5 }, { renderURL: componentURL6 }],
+ biddingLogicURL: createBiddingScriptURL({
+ generateBid: `return {bid: 1,
+ render: interestGroup.ads[0].renderURL,
+ adComponents: ["${componentURL1}", "${componentURL2}",
+ "${componentURL3}", "${componentURL4}",
+ "${componentURL5}", "${componentURL6}"]};`
+ })
+ });
+}, 'Component ads trusted scoring signals.');