// This file is adapted from /fledge/tentative/resources/fledge-util.js, // removing unnecessary logic and modifying to allow it to be run in the // private-aggregation directory. "use strict;" const FULL_URL = window.location.href; let BASE_URL = FULL_URL.substring(0, FULL_URL.lastIndexOf('/') + 1) const BASE_PATH = (new URL(BASE_URL)).pathname; const DEFAULT_INTEREST_GROUP_NAME = 'default name'; // Use python script files under fledge directory const FLEDGE_DIR = '/fledge/tentative/'; const FLEDGE_BASE_URL = BASE_URL.replace(BASE_PATH, FLEDGE_DIR); // Sleep method that waits for prescribed number of milliseconds. const sleep = ms => new Promise(resolve => step_timeout(resolve, ms)); // Generates a UUID by token. function generateUuid() { let uuid = token(); return uuid; } // Creates a URL that will be sent to the handler. // `uuid` is used to identify the stash shard to use. // `operate` is used to set action as write or read. // `report` is used to carry the message for write requests. function createReportingURL(uuid, operation, report = 'default-report') { let url = new URL(`${window.location.origin}${BASE_PATH}resources/protected_audience_event_level_report_handler.py`); url.searchParams.append('uuid', uuid); url.searchParams.append('operation', operation); if (report) url.searchParams.append('report', report); return url.toString(); } function createWritingURL(uuid, report) { return createReportingURL(uuid, 'write'); } function createReadingURL(uuid) { return createReportingURL(uuid, 'read'); } async function waitForObservedReports(uuid, expectedNumReports, timeout = 5000 /*ms*/) { expectedReports = Array(expectedNumReports).fill('default-report'); const reportURL = createReadingURL(uuid); let startTime = performance.now(); while (performance.now() - startTime < timeout) { let response = await fetch(reportURL, { credentials: 'omit', mode: 'cors' }); let actualReports = await response.json(); // If expected number of reports have been observed, compare with list of // all expected reports and exit. if (actualReports.length == expectedReports.length) { assert_array_equals(actualReports.sort(), expectedReports); return; } await sleep(/*ms=*/ 100); } assert_unreached("Report fetching timed out: " + uuid); } // 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 url = new URL(`${FLEDGE_BASE_URL}resources/bidding-logic.sub.py`); if (params.generateBid) url.searchParams.append('generateBid', params.generateBid); if (params.reportWin) url.searchParams.append('reportWin', params.reportWin); if (params.error) url.searchParams.append('error', params.error); if (params.bid) url.searchParams.append('bid', params.bid); return url.toString(); } // 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 url = new URL(`${FLEDGE_BASE_URL}resources/decision-logic.sub.py`); url.searchParams.append('uuid', uuid); if (params.scoreAd) url.searchParams.append('scoreAd', params.scoreAd); if (params.reportResult) url.searchParams.append('reportResult', params.reportResult); if (params.error) 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. function createRenderURL(uuid, script) { let url = new URL(`${FLEDGE_BASE_URL}resources/fenced-frame.sub.py`); if (script) url.searchParams.append('script', script); url.searchParams.append('uuid', uuid); return url.toString(); } // 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}". // `interestGroupOverrides` is required to override fields in the joined // interest group. async function joinInterestGroup(test, uuid, interestGroupOverrides) { const INTEREST_GROUP_LIFETIME_SECS = 60; let interestGroup = { owner: window.location.origin, name: DEFAULT_INTEREST_GROUP_NAME, ads: [{renderURL: createRenderURL(uuid)}], ...interestGroupOverrides }; await navigator.joinAdInterestGroup(interestGroup, INTEREST_GROUP_LIFETIME_SECS); test.add_cleanup( async () => {await navigator.leaveAdInterestGroup(interestGroup)}); } // Runs a FLEDGE auction and returns the result. `auctionConfigOverrides` is // required to override fields in the auction configuration. async function runBasicFledgeAuction(test, uuid, auctionConfigOverrides) { let auctionConfig = { seller: window.location.origin, interestGroupBuyers: [window.location.origin], resolveToConfig: true, ...auctionConfigOverrides }; return await navigator.runAdAuction(auctionConfig); } // 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 runBasicFledgeAuction(test, uuid, auctionConfigOverrides); assert_true(config instanceof FencedFrameConfig, `Wrong value type returned from auction: ${config.constructor.type}`); let fencedFrame = document.createElement('fencedframe'); fencedFrame.mode = 'opaque-ads'; fencedFrame.config = config; document.body.appendChild(fencedFrame); test.add_cleanup(() => { document.body.removeChild(fencedFrame); }); } // Joins an interest group and runs an auction, expecting no winner to be // returned. "testConfig" can optionally modify the interest group or // auctionConfig. async function runBasicFledgeTestExpectingNoWinner(test, testConfig) { const uuid = generateUuid(test); await joinInterestGroup(test, uuid, testConfig.interestGroupOverrides); let result = await runBasicFledgeAuction( test, uuid, testConfig.auctionConfigOverrides); assert_true(result === null, 'Auction unexpectedly had a winner'); } // Test helper for report phase of auctions that lets the caller specify the // body of scoreAd(), reportResult(), generateBid() and reportWin(), as well as // additional arguments to be passed to joinAdInterestGroup() and runAdAuction() async function runReportTest(test, uuid, codeToInsert, expectedNumReports = 0, overrides = {}) { let generateBid = codeToInsert.generateBid; let scoreAd = codeToInsert.scoreAd; let reportWin = codeToInsert.reportWin; let reportResult = codeToInsert.reportResult; let extraInterestGroupOverrides = overrides.joinAdInterestGroup || {} let extraAuctionConfigOverrides = overrides.runAdAuction || {} let interestGroupOverrides = { biddingLogicURL: createBiddingScriptURL({ generateBid, reportWin }), ...extraInterestGroupOverrides }; let auctionConfigOverrides = { decisionLogicURL: createDecisionScriptURL( uuid, { scoreAd, reportResult }), ...extraAuctionConfigOverrides } await joinInterestGroup(test, uuid, interestGroupOverrides); await runBasicFledgeAuctionAndNavigate(test, uuid, auctionConfigOverrides); if (expectedNumReports) { await waitForObservedReports(uuid, expectedNumReports); } }