1147 lines
49 KiB
JavaScript
1147 lines
49 KiB
JavaScript
"use strict";
|
|
|
|
const BASE_URL = document.baseURI.substring(0, document.baseURI.lastIndexOf('/') + 1);
|
|
const BASE_PATH = (new URL(BASE_URL)).pathname;
|
|
|
|
// Allow overriding to allow other repositories to use these utility functions.
|
|
let 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]}}';
|
|
|
|
// Trusted signals hosted on OTHER_ORIGIN1
|
|
const CROSS_ORIGIN_TRUSTED_BIDDING_SIGNALS_URL = OTHER_ORIGIN1 + BASE_PATH +
|
|
'resources/trusted-bidding-signals.py';
|
|
const CROSS_ORIGIN_TRUSTED_SCORING_SIGNALS_URL = OTHER_ORIGIN1 + BASE_PATH +
|
|
'resources/trusted-scoring-signals.py';
|
|
|
|
// 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}${RESOURCE_PATH}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}`);
|
|
}
|
|
|
|
function createHighestScoringOtherBidReportURL(uuid, highestScoringOtherBid) {
|
|
return createSellerReportURL(uuid) + '&highestScoringOtherBid=' + Math.round(highestScoringOtherBid);
|
|
}
|
|
|
|
// 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}${RESOURCE_PATH}direct-from-seller-signals.py`);
|
|
return url.toString();
|
|
}
|
|
|
|
function createUpdateURL(params = {}) {
|
|
let origin = window.location.origin;
|
|
let url = new URL(`${origin}${RESOURCE_PATH}update-url.py`);
|
|
url.searchParams.append('body', params.body);
|
|
url.searchParams.append('uuid', params.uuid);
|
|
|
|
return url.toString();
|
|
}
|
|
|
|
// Generates a UUID and registers a cleanup method with the test fixture to
|
|
// request a URL from the request tracking script that clears all data
|
|
// associated with the generated uuid when requested.
|
|
function generateUuid(test) {
|
|
let uuid = token();
|
|
test.add_cleanup(async () => {
|
|
let response = await fetch(createCleanupURL(uuid),
|
|
{ credentials: 'omit', mode: 'cors' });
|
|
assert_equals(await response.text(), 'cleanup complete',
|
|
`Sever state cleanup failed`);
|
|
});
|
|
return uuid;
|
|
}
|
|
|
|
// Helper to fetch "tracked_data" URL to fetch all data recorded by the
|
|
// tracker URL associated with "uuid". Throws on error, including if
|
|
// the retrieved object's errors field is non-empty.
|
|
async function fetchTrackedData(uuid) {
|
|
let trackedRequestsURL = createTrackerURL(window.location.origin, uuid,
|
|
'tracked_data');
|
|
let response = await fetch(trackedRequestsURL,
|
|
{ credentials: 'omit', mode: 'cors' });
|
|
let trackedData = await response.json();
|
|
|
|
// Fail on fetch error.
|
|
if (trackedData.error) {
|
|
throw trackedRequestsURL + ' fetch failed:' + JSON.stringify(trackedData);
|
|
}
|
|
|
|
// Fail on errors reported by the tracker script.
|
|
if (trackedData.errors.length > 0) {
|
|
throw 'Errors reported by request-tracker.py:' +
|
|
JSON.stringify(trackedData.errors);
|
|
}
|
|
|
|
return trackedData;
|
|
}
|
|
|
|
// Repeatedly requests "tracked_data" URL until exactly the entries in
|
|
// "expectedRequests" have been observed by the request tracker script (in
|
|
// any order, since report URLs are not guaranteed to be sent in any order).
|
|
//
|
|
// Elements of `expectedRequests` should either be URLs, in the case of GET
|
|
// requests, or "<URL>, body: <body>" in the case of POST requests.
|
|
//
|
|
// `filter` will be applied to the array of tracked 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, filter) {
|
|
// Sort array for easier comparison, as observed request order does not
|
|
// matter, and replace UUID to print consistent errors on failure.
|
|
expectedRequests = expectedRequests.map((url) => url.replace(uuid, '<uuid>')).sort();
|
|
|
|
while (true) {
|
|
let trackedData = await fetchTrackedData(uuid);
|
|
|
|
// Clean up "trackedRequests" in same manner as "expectedRequests".
|
|
let trackedRequests = trackedData.trackedRequests.map(
|
|
(url) => url.replace(uuid, '<uuid>')).sort();
|
|
|
|
if (filter) {
|
|
trackedRequests = trackedRequests.filter(filter);
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
// If expected number of requests have been observed, compare with list of
|
|
// all expected requests and exit. This check was previously before the for loop,
|
|
// but was swapped in order to avoid flakiness with failing tests and their
|
|
// respective *-expected.txt.
|
|
if (trackedRequests.length >= expectedRequests.length) {
|
|
assert_array_equals(trackedRequests, expectedRequests);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// Similar to waitForObservedRequests, but ignore forDebuggingOnly reports.
|
|
async function waitForObservedRequestsIgnoreDebugOnlyReports(
|
|
uuid, expectedRequests) {
|
|
return waitForObservedRequests(
|
|
uuid,
|
|
expectedRequests,
|
|
request => !request.includes('forDebuggingOnly'));
|
|
}
|
|
|
|
// 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.reportAdditionalBidWin != null)
|
|
url.searchParams.append('reportAdditionalBidWin', params.reportAdditionalBidWin);
|
|
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);
|
|
if (params.permitCrossOriginTrustedSignals != null) {
|
|
url.searchParams.append('permit-cross-origin-trusted-signals',
|
|
params.permitCrossOriginTrustedSignals);
|
|
}
|
|
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 "==" and "!=" 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
|
|
};
|
|
}
|
|
|
|
// Waits for the join command to complete. Adds cleanup command to `test` to
|
|
// leave the interest group when the test completes.
|
|
async function joinInterestGroupWithoutDefaults(test, interestGroup,
|
|
durationSeconds = 60) {
|
|
await navigator.joinAdInterestGroup(interestGroup, durationSeconds);
|
|
await makeInterestGroupKAnonymous(interestGroup);
|
|
test.add_cleanup(
|
|
async () => { await navigator.leaveAdInterestGroup(interestGroup); });
|
|
}
|
|
|
|
// 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) {
|
|
await joinInterestGroupWithoutDefaults(
|
|
test, createInterestGroupForOrigin(
|
|
uuid, window.location.origin, interestGroupOverrides),
|
|
durationSeconds);
|
|
}
|
|
|
|
// Joins a negative interest group with the specified owner, name, and
|
|
// additionalBidKey. Because these are the only valid fields for a negative
|
|
// interest groups, this function doesn't expose an 'overrides' parameter.
|
|
// Adds cleanup command to `test` to leave the interest group when the test
|
|
// completes.
|
|
async function joinNegativeInterestGroup(
|
|
test, owner, name, additionalBidKey) {
|
|
let interestGroup = {
|
|
owner: owner,
|
|
name: name,
|
|
additionalBidKey: additionalBidKey
|
|
};
|
|
if (owner !== window.location.origin) {
|
|
let iframe = await createIframe(test, owner, 'join-ad-interest-group');
|
|
await runInFrame(
|
|
test, iframe,
|
|
`await joinInterestGroupWithoutDefaults(` +
|
|
`test_instance, ${JSON.stringify(interestGroup)})`);
|
|
} else {
|
|
await joinInterestGroupWithoutDefaults(test, 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);
|
|
}
|
|
|
|
// Helper function for running a standard test of the additional bid and
|
|
// negative targeting features. This helper verifies that the auction produces a
|
|
// winner. It takes the following arguments:
|
|
// - test/uuid: the test object and uuid from the test case (see generateUuid)
|
|
// - buyers: array of strings, each a domain for a buyer participating in this
|
|
// auction
|
|
// - auctionNonce: string, the auction nonce for this auction, typically
|
|
// retrieved from a prior call to navigator.createAuctionNonce
|
|
// - additionalBidsPromise: promise resolving to undefined, to be resolved when
|
|
// the additional bids have been retrieved with fetch().
|
|
// - highestScoringOtherBid: the amount of the second-highest bid,
|
|
// or zero if there's no second-highest bid
|
|
// - winningAdditionalBidId: the label of the winning bid
|
|
async function runAdditionalBidTest(test, uuid, buyers, auctionNonce,
|
|
additionalBidsPromise,
|
|
highestScoringOtherBid,
|
|
winningAdditionalBidId) {
|
|
await runBasicFledgeAuctionAndNavigate(
|
|
test, uuid,
|
|
{ interestGroupBuyers: buyers,
|
|
auctionNonce: auctionNonce,
|
|
additionalBids: additionalBidsPromise,
|
|
decisionLogicURL: createDecisionScriptURL(
|
|
uuid,
|
|
{ reportResult: `sendReportTo("${createSellerReportURL(uuid)}&highestScoringOtherBid=" + Math.round(browserSignals.highestScoringOtherBid));` })});
|
|
|
|
await waitForObservedRequests(
|
|
uuid, [createHighestScoringOtherBidReportURL(uuid, highestScoringOtherBid),
|
|
createBidderReportURL(uuid, winningAdditionalBidId)]);
|
|
}
|
|
|
|
// Similar to runAdditionalBidTest(), but expects no winner. It takes the
|
|
// following arguments:
|
|
// - test/uuid: the test object and uuid from the test case (see generateUuid)
|
|
// - buyers: array of strings, each a domain for a buyer participating in this
|
|
// auction
|
|
// - auctionNonce: string, the auction nonce for this auction, typically
|
|
// retrieved from a prior call to navigator.createAuctionNonce
|
|
// - additionalBidsPromise: promise resolving to undefined, to be resolved when
|
|
// the additional bids have been retrieved with fetch().
|
|
async function runAdditionalBidTestNoWinner(
|
|
test, uuid, buyers, auctionNonce, additionalBidsPromise) {
|
|
await runBasicFledgeTestExpectingNoWinner(test, uuid, {
|
|
interestGroupBuyers: buyers,
|
|
auctionNonce: auctionNonce,
|
|
additionalBids: additionalBidsPromise,
|
|
decisionLogicURL: createDecisionScriptURL(uuid)
|
|
});
|
|
}
|
|
|
|
// 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})`);
|
|
}
|
|
|
|
// Leaves a cross-origin interest group, by running a leave in an iframe.
|
|
async function leaveCrossOriginInterestGroup(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 leaveInterestGroup(${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 topLevelWindow = await createTopLevelWindow(test, origin);
|
|
await runInFrame(test, topLevelWindow,
|
|
`await joinInterestGroup(test_instance, "${uuid}", ${interestGroup})`);
|
|
}
|
|
|
|
// Opens a top-level window and calls joinCrossOriginInterestGroup() in it.
|
|
async function joinCrossOriginInterestGroupInTopLevelWindow(
|
|
test, uuid, windowOrigin, interestGroupOrigin, interestGroupOverrides = {}) {
|
|
let interestGroup = JSON.stringify(
|
|
createInterestGroupForOrigin(uuid, interestGroupOrigin, interestGroupOverrides));
|
|
|
|
let topLevelWindow = await createTopLevelWindow(test, windowOrigin);
|
|
await runInFrame(test, topLevelWindow,
|
|
`await joinCrossOriginInterestGroup(
|
|
test_instance, "${uuid}", "${interestGroupOrigin}", ${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)}");`,
|
|
};
|
|
}
|
|
|
|
let additionalBidHelper = function() {
|
|
|
|
// Creates an additional bid with the given parameters. This additional bid
|
|
// specifies a biddingLogicURL that provides an implementation of
|
|
// reportAdditionalBidWin that triggers a sendReportTo() to the bidder report
|
|
// URL of the winning additional bid. Additional bids are described in more
|
|
// detail at
|
|
// https://github.com/WICG/turtledove/blob/main/FLEDGE.md#6-additional-bids.
|
|
// Returned bids have an additional `testMetadata` field that's modified by
|
|
// several of the other helper functions defined below and is consumed by
|
|
// `fetchAdditionalBids()`. Created additional bids must be used only once,
|
|
// as `fetchAdditionalBids()` consumes and discards the `testMetadata` field.
|
|
function createAdditionalBid(uuid, seller, buyer, interestGroupName, bidAmount) {
|
|
return {
|
|
interestGroup: {
|
|
name: interestGroupName,
|
|
biddingLogicURL: createBiddingScriptURL({
|
|
origin: buyer,
|
|
reportAdditionalBidWin: `sendReportTo("${
|
|
createBidderReportURL(uuid, interestGroupName)}");`
|
|
}),
|
|
owner: buyer
|
|
},
|
|
bid: {ad: ['metadata'], bid: bidAmount, render: createRenderURL(uuid)},
|
|
seller: seller,
|
|
testMetadata: {}
|
|
};
|
|
}
|
|
|
|
// Sets the auction nonce that will be included by the server on the
|
|
// 'Ad-Auction-Additional-Bid' response header for this bid. All valid
|
|
// additional bids should have an auctionNonce in the header, so this
|
|
// should be called by most tests.
|
|
function setAuctionNonceInHeader(additionalBid, auctionNonce) {
|
|
additionalBid.testMetadata.auctionNonce = auctionNonce;
|
|
}
|
|
|
|
// Sets the seller nonce that will be included by the server on the
|
|
// 'Ad-Auction-Additional-Bid' response header for this bid.
|
|
function setSellerNonceInHeader(additionalBid, sellerNonce) {
|
|
additionalBid.testMetadata.sellerNonce = sellerNonce;
|
|
}
|
|
|
|
// Tells `fetchAdditionalBids` to correctly sign the additional bid with
|
|
// the given secret keys before returning that as a signed additional bid.
|
|
// The signatures aren't computed yet because `additionalBid` - whose string
|
|
// representation is signed - may still change between when this is called
|
|
// and when `fetchAdditionalBids` is called.
|
|
function signWithSecretKeys(additionalBid, secretKeys) {
|
|
additionalBid.testMetadata.secretKeysForValidSignatures = secretKeys;
|
|
}
|
|
|
|
// Tells the additional bid endpoint to incorrectly sign the additional bid
|
|
// with the given secret keys before returning that as a signed additional
|
|
// bid. This is used for testing the behavior when the auction encounters an
|
|
// invalid signature. The signatures aren't computed yet because
|
|
// `additionalBid` - whose string representation is signed - may still change
|
|
// between when this is called and when `fetchAdditionalBids` is called.
|
|
function incorrectlySignWithSecretKeys(additionalBid, secretKeys) {
|
|
additionalBid.testMetadata.secretKeysForInvalidSignatures = secretKeys;
|
|
}
|
|
|
|
// Takes the auctionNonce and sellerNonce as strings, and combines them with
|
|
// SHA256, returning the result as a base64 string.
|
|
async function computeBidNonce(auctionNonce, sellerNonce) {
|
|
// Compute the bidNonce as hashed bytes.
|
|
const combined_utf8 = new TextEncoder().encode(auctionNonce + sellerNonce);
|
|
const hashed = await crypto.subtle.digest('SHA-256',combined_utf8);
|
|
|
|
// Convert the hashed bytes to base64.
|
|
return btoa(String.fromCharCode(...new Uint8Array(hashed)));
|
|
}
|
|
|
|
// Adds a single negative interest group to an additional bid, as described at:
|
|
// https://github.com/WICG/turtledove/blob/main/FLEDGE.md#622-how-additional-bids-specify-their-negative-interest-groups
|
|
function addNegativeInterestGroup(additionalBid, negativeInterestGroup) {
|
|
additionalBid.negativeInterestGroup = negativeInterestGroup;
|
|
}
|
|
|
|
// Adds multiple negative interest groups to an additional bid, as described at:
|
|
// https://github.com/WICG/turtledove/blob/main/FLEDGE.md#622-how-additional-bids-specify-their-negative-interest-groups
|
|
function addNegativeInterestGroups(
|
|
additionalBid, negativeInterestGroups, joiningOrigin) {
|
|
additionalBid.negativeInterestGroups = {
|
|
joiningOrigin: joiningOrigin,
|
|
interestGroupNames: negativeInterestGroups
|
|
};
|
|
}
|
|
|
|
const _ed25519ModulePromise =
|
|
import('../third_party/noble-ed25519/noble-ed25519.js');
|
|
|
|
// Returns a signature entry for a signed additional bid.
|
|
//
|
|
// `message` is the additional bid text (or other text if generating an
|
|
// invalid signature) to sign.
|
|
//
|
|
// `base64EncodedSecretKey` is the base64-encoded Ed25519 key with which to
|
|
// sign the message. From this secret key, the public key can be deduced,
|
|
// which becomes part of the signature entry.
|
|
async function _generateSignature(message, base64EncodedSecretKey) {
|
|
const ed25519 = await _ed25519ModulePromise;
|
|
const secretKey =
|
|
Uint8Array.from(atob(base64EncodedSecretKey), c => c.charCodeAt(0));
|
|
const [publicKey, signature] = await Promise.all([
|
|
ed25519.getPublicKeyAsync(secretKey),
|
|
ed25519.signAsync(new TextEncoder().encode(message), secretKey)
|
|
]);
|
|
|
|
return {
|
|
'key': btoa(String.fromCharCode(...publicKey)),
|
|
'signature': btoa(String.fromCharCode(...signature))
|
|
};
|
|
}
|
|
|
|
// Returns a signed additional bid given an additional bid and secret keys.
|
|
// `additionalBid` is the additional bid to sign. It must not contain a
|
|
// `testMetadata` - that should have been removed prior to calling this.
|
|
//
|
|
// `secretKeysForValidSignatures` is a list of strings, each a base64-encoded
|
|
// Ed25519 secret key with which to sign the additional bid, whereas
|
|
// `secretKeysForInvalidSignatures` is a list of strings, each a
|
|
// base64-encoded Ed25519 secret key with which to *incorrectly* sign the
|
|
// additional bid.
|
|
async function _signAdditionalBid(
|
|
additionalBid, secretKeysForValidSignatures,
|
|
secretKeysForInvalidSignatures) {
|
|
async function _signString(string, secretKeys) {
|
|
if (!secretKeys) {
|
|
return [];
|
|
}
|
|
return await Promise.all(secretKeys.map(
|
|
async secretKey => await _generateSignature(
|
|
string, secretKey)));
|
|
}
|
|
|
|
assert_not_own_property(
|
|
additionalBid, 'testMetadata',
|
|
'testMetadata should be removed from additionalBid before signing');
|
|
const additionalBidString = JSON.stringify(additionalBid);
|
|
let [validSignatures, invalidSignatures] = await Promise.all([
|
|
_signString(additionalBidString, secretKeysForValidSignatures),
|
|
|
|
// For invalid signatures, we use the correct secret key to sign a
|
|
// different message - the additional bid prepended by 'invalid' - so
|
|
// that the signature is a structually valid signature with the correct
|
|
// (public) key, but can't be used to verify the additional bid.
|
|
_signString('invalid' + additionalBidString, secretKeysForInvalidSignatures)
|
|
]);
|
|
return {
|
|
'bid': additionalBidString,
|
|
'signatures': validSignatures.concat(invalidSignatures)
|
|
};
|
|
}
|
|
|
|
// Given an additionalBid object, this returns a string to be used as the
|
|
// value of the `Ad-Auction-Additional-Bid` response header. To produce this
|
|
// header, this signs the signing the `additionalBid` with the signatures
|
|
// specified by prior calls to `signWithSecretKeys` and
|
|
// `incorrectlySignWithSecretKeys` above; base64-encodes the stringified
|
|
// `signedAdditionalBid`; and then prepends that with the `auctionNonce` and/or
|
|
// `sellerNonce` specified by prior calls to `setAuctionNonceInHeader` and
|
|
// `setSellerNonceInHeader` above, respectively.
|
|
async function _convertAdditionalBidToResponseHeader(additionalBid) {
|
|
const testMetadata = additionalBid.testMetadata;
|
|
delete additionalBid.testMetadata;
|
|
|
|
const signedAdditionalBid = await _signAdditionalBid(
|
|
additionalBid,
|
|
testMetadata.secretKeysForValidSignatures,
|
|
testMetadata.secretKeysForInvalidSignatures);
|
|
|
|
|
|
return [
|
|
testMetadata.auctionNonce,
|
|
testMetadata.sellerNonce,
|
|
btoa(JSON.stringify(signedAdditionalBid))
|
|
].filter(k => k !== undefined).join(':');
|
|
}
|
|
|
|
// Fetch some number of fully prepared additional bid from a seller and verify
|
|
// that the `Ad-Auction-Additional-Bid` header is not visible in this
|
|
// JavaScript context. The `additionalBids` parameter is a list of additional
|
|
// bids objects created by `createAdditionalBid` and modified by other
|
|
// functions on this helper. Once passed to this method, additional bids may
|
|
// not be reused in a future call to `fetchAdditionalBids()`, since this
|
|
// mothod consumes and destroys their `testMetadata` field.
|
|
async function fetchAdditionalBids(seller, additionalBids) {
|
|
let additionalBidHeaderValues = await Promise.all(additionalBids.map(
|
|
async additionalBid =>
|
|
await _convertAdditionalBidToResponseHeader(additionalBid)));
|
|
|
|
const url = new URL(`${seller}${RESOURCE_PATH}additional-bids.py`);
|
|
url.searchParams.append(
|
|
'additionalBidHeaderValues', JSON.stringify(additionalBidHeaderValues));
|
|
const response = await fetch(url.href, {adAuctionHeaders: true});
|
|
|
|
assert_equals(response.status, 200, 'Failed to fetch additional bid: ' + await response.text());
|
|
assert_false(
|
|
response.headers.has('Ad-Auction-Additional-Bid'),
|
|
'Header "Ad-Auction-Additional-Bid" should not be available in JavaScript context.');
|
|
}
|
|
|
|
return {
|
|
createAdditionalBid: createAdditionalBid,
|
|
setAuctionNonceInHeader: setAuctionNonceInHeader,
|
|
setSellerNonceInHeader: setSellerNonceInHeader,
|
|
signWithSecretKeys: signWithSecretKeys,
|
|
incorrectlySignWithSecretKeys: incorrectlySignWithSecretKeys,
|
|
computeBidNonce: computeBidNonce,
|
|
addNegativeInterestGroup: addNegativeInterestGroup,
|
|
addNegativeInterestGroups: addNegativeInterestGroups,
|
|
fetchAdditionalBids: fetchAdditionalBids
|
|
};
|
|
}();
|
|
|
|
|
|
// DeprecatedRenderURLReplacements helper function.
|
|
// Returns an object containing sample strings both before and after the
|
|
// replacements in 'replacements' have been applied by
|
|
// deprecatedRenderURLReplacements. All substitution strings will appear
|
|
// only once in the output strings.
|
|
function createStringBeforeAndAfterReplacements(deprecatedRenderURLReplacements) {
|
|
let beforeReplacements = '';
|
|
let afterReplacements = '';
|
|
if(deprecatedRenderURLReplacements){
|
|
for (const [match, replacement] of Object.entries(deprecatedRenderURLReplacements)) {
|
|
beforeReplacements += match + "/";
|
|
afterReplacements += replacement + "/";
|
|
}
|
|
}
|
|
return { beforeReplacements, afterReplacements };
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
async function makeInterestGroupKAnonymous(passedInterestGroup) {
|
|
// Make a copy so we can sanitize fields without affecting the tests.
|
|
let interestGroup = structuredClone(passedInterestGroup);
|
|
const ownerURL = new URL(interestGroup.owner);
|
|
interestGroup.owner = ownerURL.origin;
|
|
interestGroup.name = String(interestGroup.name).toWellFormed();
|
|
interestGroup.biddingLogicURL =
|
|
(new URL(interestGroup.biddingLogicURL, BASE_URL)).toString();
|
|
|
|
function b64(array) {
|
|
return btoa(String.fromCharCode.apply(null, array));
|
|
}
|
|
let hashes = [];
|
|
if (Array.isArray(interestGroup.ads)) {
|
|
for (const ad of interestGroup.ads) {
|
|
hashes.push(b64(await computeKeyHashOfAd(interestGroup, ad)));
|
|
hashes.push(
|
|
b64(await computeKeyHashOfReportingId(interestGroup, ad, null)));
|
|
if (Array.isArray(ad.selectableBuyerAndSellerReportingIds)) {
|
|
for (const id of ad.selectableBuyerAndSellerReportingIds) {
|
|
hashes.push(
|
|
b64(await computeKeyHashOfReportingId(interestGroup, ad, id)));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (Array.isArray(interestGroup.adComponents)) {
|
|
for (const ad of interestGroup.adComponents) {
|
|
hashes.push(b64(await computeKeyHashOfComponentAd(interestGroup, ad)));
|
|
}
|
|
}
|
|
await test_driver.set_protected_audience_k_anonymity(
|
|
interestGroup.owner, interestGroup.name, hashes);
|
|
}
|
|
|
|
async function computeKeyHashOfAd(ig, ad) {
|
|
const encoder = new TextEncoder();
|
|
const kAnonKey = encoder.encode(
|
|
`AdBid\n${ig.owner}/\n${ig.biddingLogicURL}\n${ad.renderURL}`);
|
|
return new Uint8Array(await window.crypto.subtle.digest('SHA-256', kAnonKey));
|
|
}
|
|
|
|
async function computeKeyHashOfReportingId(ig, ad, selectedReportingId = null) {
|
|
const encoder = new TextEncoder();
|
|
let kAnonKey = null;
|
|
if (!selectedReportingId) {
|
|
if (ad.buyerAndSellerReportingId) {
|
|
kAnonKey = encoder.encode(
|
|
`BuyerAndSellerReportId\n${ig.owner}/\n${ig.biddingLogicURL}\n${
|
|
ad.renderURL}\n${ad.buyerAndSellerReportingId}`);
|
|
} else if (ad.buyerReportingId) {
|
|
kAnonKey = encoder.encode(`BuyerReportId\n${ig.owner}/\n${
|
|
ig.biddingLogicURL}\n${ad.renderURL}\n${ad.buyerReportingId}`);
|
|
} else {
|
|
kAnonKey = encoder.encode(`NameReport\n${ig.owner}/\n${
|
|
ig.biddingLogicURL}\n${ad.renderURL}\n${ig.name}`);
|
|
}
|
|
} else {
|
|
function encodeKeyPartInto(part, array) {
|
|
array[0] = 0x0a;
|
|
if (!part) {
|
|
for (let i = 1; i < 6; i++) {
|
|
array[i] = 0x00;
|
|
}
|
|
return 6;
|
|
}
|
|
const len = part.length;
|
|
array[1] = 0x01;
|
|
array[2] = (len >> 24) % 256
|
|
array[3] = (len >> 16) % 256
|
|
array[4] = (len >> 8) % 256
|
|
array[5] = len % 256;
|
|
encoder.encodeInto(part, array.subarray(6));
|
|
return 1 + 5 + len;
|
|
}
|
|
const baseText = `SelectedBuyerAndSellerReportId\n${ig.owner}/\n${
|
|
ig.biddingLogicURL}\n${ad.renderURL}`;
|
|
const selectedReportingIdLen =
|
|
1 + 5 + (selectedReportingId ? selectedReportingId.length : 0);
|
|
const buyerAndSellerReportingIdLen = 1 + 5 +
|
|
(ad.buyerAndSellerReportingId ? ad.buyerAndSellerReportingId.length : 0)
|
|
const buyerReportingIdLen =
|
|
1 + 5 + (ad.buyerReportingId ? ad.buyerReportingId.length : 0)
|
|
const expectedLen = baseText.length + selectedReportingIdLen +
|
|
buyerAndSellerReportingIdLen + buyerReportingIdLen;
|
|
kAnonKey = new Uint8Array(expectedLen);
|
|
let actualLen = 0;
|
|
actualLen += encoder.encodeInto(baseText, kAnonKey).written;
|
|
actualLen +=
|
|
encodeKeyPartInto(selectedReportingId, kAnonKey.subarray(actualLen));
|
|
actualLen += encodeKeyPartInto(
|
|
ad.buyerAndSellerReportingId, kAnonKey.subarray(actualLen));
|
|
actualLen +=
|
|
encodeKeyPartInto(ad.buyerReportingId, kAnonKey.subarray(actualLen));
|
|
}
|
|
return new Uint8Array(await window.crypto.subtle.digest('SHA-256', kAnonKey));
|
|
}
|
|
|
|
async function computeKeyHashOfComponentAd(ig, ad) {
|
|
const encoder = new TextEncoder();
|
|
const kAnonKey = encoder.encode(`ComponentBid\n${ad.renderURL}`);
|
|
return new Uint8Array(await window.crypto.subtle.digest('SHA-256', kAnonKey));
|
|
}
|