From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- testing/web-platform/tests/fledge/tentative/TODO | 90 +++ .../tests/fledge/tentative/abort.https.window.js | 103 +++ ...ction-config-passed-to-worklets.https.window.js | 208 +++++ .../tentative/auction-config.https.window.js | 392 +++++++++ ...rigin-joined-ad-interest-groups.https.window.js | 312 ++++++++ .../fledge/tentative/component-ads.https.window.js | 449 +++++++++++ .../tentative/component-auction.https.window.js | 719 +++++++++++++++++ .../fledge/tentative/cross-origin.https.window.js | 452 +++++++++++ .../fledge/tentative/currency.https.window.js | 874 +++++++++++++++++++++ .../direct-from-seller-signals.https.window.js | 616 +++++++++++++++ ...on-headers-insecure-context.tentative.http.html | 11 + .../fetch-ad-auction-headers.tentative.https.html | 12 + .../tentative/generate-bid-recency.https.window.js | 34 + .../fledge/tentative/insecure-context.window.js | 8 + ...st-group-passed-to-generate-bid.https.window.js | 737 +++++++++++++++++ ...-interest-group-in-fenced-frame.https.window.js | 369 +++++++++ .../join-leave-ad-interest-group.https.window.js | 604 ++++++++++++++ .../kanon-status-below-threshold.https.window.js | 20 + .../kanon-status-not-calculated.https.window.js | 20 + .../tests/fledge/tentative/network.https.window.js | 327 ++++++++ .../fledge/tentative/no-winner.https.window.js | 106 +++ .../tentative/register-ad-beacon.https.window.js | 307 ++++++++ .../tentative/reporting-arguments.https.window.js | 305 +++++++ .../tentative/resources/bidding-logic.sub.py | 71 ++ .../tentative/resources/decision-logic.sub.py | 59 ++ .../resources/direct-from-seller-signals.py | 144 ++++ .../tests/fledge/tentative/resources/empty.html | 1 + .../fledge/tentative/resources/fenced-frame.sub.py | 26 + .../fledge/tentative/resources/fledge-util.sub.js | 645 +++++++++++++++ .../tentative/resources/fledge_http_server_util.py | 67 ++ .../fledge/tentative/resources/incrementer.wasm | Bin 0 -> 46 bytes .../resources/redirect-to-trusted-signals.py | 22 + .../tests/fledge/tentative/resources/redirect.py | 8 + .../fledge/tentative/resources/request-tracker.py | 113 +++ .../fledge/tentative/resources/set-cookie.asis | 3 + .../tentative/resources/subordinate-frame.sub.html | 89 +++ .../resources/subordinate-frame.sub.html.headers | 1 + .../tentative/resources/trusted-bidding-signals.py | 133 ++++ .../tentative/resources/trusted-scoring-signals.py | 144 ++++ .../fledge/tentative/resources/wasm-helper.py | 38 + .../fledge/tentative/resources/worklet-helpers.js | 23 + .../fledge/tentative/round-a-value.https.window.js | 161 ++++ .../tentative/send-report-to.https.window.js | 155 ++++ .../tests/fledge/tentative/tie.https.window.js | 125 +++ .../trusted-bidding-signals.https.window.js | 787 +++++++++++++++++++ .../trusted-scoring-signals.https.window.js | 512 ++++++++++++ 46 files changed, 10402 insertions(+) create mode 100644 testing/web-platform/tests/fledge/tentative/TODO create mode 100644 testing/web-platform/tests/fledge/tentative/abort.https.window.js create mode 100644 testing/web-platform/tests/fledge/tentative/auction-config-passed-to-worklets.https.window.js create mode 100644 testing/web-platform/tests/fledge/tentative/auction-config.https.window.js create mode 100644 testing/web-platform/tests/fledge/tentative/clear-origin-joined-ad-interest-groups.https.window.js create mode 100644 testing/web-platform/tests/fledge/tentative/component-ads.https.window.js create mode 100644 testing/web-platform/tests/fledge/tentative/component-auction.https.window.js create mode 100644 testing/web-platform/tests/fledge/tentative/cross-origin.https.window.js create mode 100644 testing/web-platform/tests/fledge/tentative/currency.https.window.js create mode 100644 testing/web-platform/tests/fledge/tentative/direct-from-seller-signals.https.window.js create mode 100644 testing/web-platform/tests/fledge/tentative/fetch-ad-auction-headers-insecure-context.tentative.http.html create mode 100644 testing/web-platform/tests/fledge/tentative/fetch-ad-auction-headers.tentative.https.html create mode 100644 testing/web-platform/tests/fledge/tentative/generate-bid-recency.https.window.js create mode 100644 testing/web-platform/tests/fledge/tentative/insecure-context.window.js create mode 100644 testing/web-platform/tests/fledge/tentative/interest-group-passed-to-generate-bid.https.window.js create mode 100644 testing/web-platform/tests/fledge/tentative/join-leave-ad-interest-group-in-fenced-frame.https.window.js create mode 100644 testing/web-platform/tests/fledge/tentative/join-leave-ad-interest-group.https.window.js create mode 100644 testing/web-platform/tests/fledge/tentative/kanon-status-below-threshold.https.window.js create mode 100644 testing/web-platform/tests/fledge/tentative/kanon-status-not-calculated.https.window.js create mode 100644 testing/web-platform/tests/fledge/tentative/network.https.window.js create mode 100644 testing/web-platform/tests/fledge/tentative/no-winner.https.window.js create mode 100644 testing/web-platform/tests/fledge/tentative/register-ad-beacon.https.window.js create mode 100644 testing/web-platform/tests/fledge/tentative/reporting-arguments.https.window.js create mode 100644 testing/web-platform/tests/fledge/tentative/resources/bidding-logic.sub.py create mode 100644 testing/web-platform/tests/fledge/tentative/resources/decision-logic.sub.py create mode 100644 testing/web-platform/tests/fledge/tentative/resources/direct-from-seller-signals.py create mode 100644 testing/web-platform/tests/fledge/tentative/resources/empty.html create mode 100644 testing/web-platform/tests/fledge/tentative/resources/fenced-frame.sub.py create mode 100644 testing/web-platform/tests/fledge/tentative/resources/fledge-util.sub.js create mode 100644 testing/web-platform/tests/fledge/tentative/resources/fledge_http_server_util.py create mode 100644 testing/web-platform/tests/fledge/tentative/resources/incrementer.wasm create mode 100644 testing/web-platform/tests/fledge/tentative/resources/redirect-to-trusted-signals.py create mode 100644 testing/web-platform/tests/fledge/tentative/resources/redirect.py create mode 100644 testing/web-platform/tests/fledge/tentative/resources/request-tracker.py create mode 100644 testing/web-platform/tests/fledge/tentative/resources/set-cookie.asis create mode 100644 testing/web-platform/tests/fledge/tentative/resources/subordinate-frame.sub.html create mode 100644 testing/web-platform/tests/fledge/tentative/resources/subordinate-frame.sub.html.headers create mode 100644 testing/web-platform/tests/fledge/tentative/resources/trusted-bidding-signals.py create mode 100644 testing/web-platform/tests/fledge/tentative/resources/trusted-scoring-signals.py create mode 100644 testing/web-platform/tests/fledge/tentative/resources/wasm-helper.py create mode 100644 testing/web-platform/tests/fledge/tentative/resources/worklet-helpers.js create mode 100644 testing/web-platform/tests/fledge/tentative/round-a-value.https.window.js create mode 100644 testing/web-platform/tests/fledge/tentative/send-report-to.https.window.js create mode 100644 testing/web-platform/tests/fledge/tentative/tie.https.window.js create mode 100644 testing/web-platform/tests/fledge/tentative/trusted-bidding-signals.https.window.js create mode 100644 testing/web-platform/tests/fledge/tentative/trusted-scoring-signals.https.window.js (limited to 'testing/web-platform/tests/fledge/tentative') 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 @@ + + + + + + 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 @@ + + + + + + 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 @@ + 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 """ + + + + + + + + + + + + + + """ + + 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 ", 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, '')); + + 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, '')); + + // 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 Binary files /dev/null and b/testing/web-platform/tests/fledge/tentative/resources/incrementer.wasm 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 +# ", 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 @@ + + + + + + + + + + + + + + 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: , urls: } pairs, where 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.'); -- cgit v1.2.3