Bug 1824764 [wpt PR 39217] - [FLEDGE] Add WPT tests for bidding signals., a=testonly
[gecko.git] / testing / web-platform / tests / fledge / tentative / resources / fledge-util.js
blob3e47af8576fbc8c4e03c212975ae1fb916ef65f4
1 "use strict;"
3 const FULL_URL = window.location.href;
4 const BASE_URL = FULL_URL.substring(0, FULL_URL.lastIndexOf('/') + 1);
5 const BASE_PATH = (new URL(BASE_URL)).pathname;
7 const DEFAULT_INTEREST_GROUP_NAME = 'default name';
9 // Unlike other URLs, the trustedBiddingSignalsUrl can't have a query string
10 // that's set by tests, since FLEDGE controls it entirely, so tests that
11 // exercise it use a fixed URL string. Special keys and interest group names
12 // control the response.
13 const TRUSTED_BIDDING_SIGNALS_URL =
14     `${BASE_URL}resources/trusted_bidding_signals.py`;
16 // Creates a URL that will be sent to the URL request tracker script.
17 // `uuid` is used to identify the stash shard to use.
18 // `dispatch` affects what the tracker script does.
19 // `id` can be used to uniquely identify tracked requests. It has no effect
20 //     on behavior of the script; it only serves to make the URL unique.
21 function createTrackerUrl(origin, uuid, dispatch, id = null) {
22   let url = new URL(`${origin}${BASE_PATH}resources/request_tracker.py`);
23   url.searchParams.append('uuid', uuid);
24   url.searchParams.append('dispatch', dispatch);
25   if (id)
26     url.searchParams.append('id', id);
27   return url.toString();
30 // Create tracked bidder/seller URLs. The only difference is the prefix added
31 // to the `id` passed to createTrackerUrl. The optional `id` field allows
32 // multiple bidder/seller report URLs to be distinguishable from each other.
33 function createBidderReportUrl(uuid, id = '1') {
34   return createTrackerUrl(window.location.origin, uuid, `track_get`,
35                           `bidder_report_${id}`);
37 function createSellerReportUrl(uuid, id = '1') {
38   return createTrackerUrl(window.location.origin, uuid, `track_get`,
39                           `seller_report_${id}`);
42 // Much like above ReportUrl methods, except designed for beacons, which
43 // are expected to be POSTs.
44 function createBidderBeaconUrl(uuid, id = '1') {
45   return createTrackerUrl(window.location.origin, uuid, `track_post`,
46                           `bidder_beacon_${id}`);
48 function createSellerBeaconUrl(uuid, id = '1') {
49   return createTrackerUrl(window.location.origin, uuid, `track_post`,
50                           `seller_beacon_${id}`);
53 // Generates a UUID and registers a cleanup method with the test fixture to
54 // request a URL from the request tracking script that clears all data
55 // associated with the generated uuid when requested.
56 function generateUuid(test) {
57   let uuid = token();
58   test.add_cleanup(async () => {
59     let cleanupUrl = createTrackerUrl(window.location.origin, uuid, 'clean_up');
60     let response = await fetch(cleanupUrl, {credentials: 'omit', mode: 'cors'});
61     assert_equals(await response.text(), 'cleanup complete',
62                   `Sever state cleanup failed`);
63   });
64   return uuid;
67 // Repeatedly requests "request_list" URL until exactly the entries in
68 // "expectedRequests" have been observed by the request tracker script (in
69 // any order, since report URLs are not guaranteed to be sent in any order).
71 // Elements of `expectedRequests` should either be URLs, in the case of GET
72 // requests, or "<URL>, body: <body>" in the case of POST requests.
74 // If any other strings are received from the tracking script, or the tracker
75 // script reports an error, fails the test.
76 async function waitForObservedRequests(uuid, expectedRequests) {
77   let trackedRequestsUrl = createTrackerUrl(window.location.origin, uuid,
78                                             'request_list');
79   // Sort array for easier comparison, since order doesn't matter.
80   expectedRequests.sort();
81   while (true) {
82     let response = await fetch(trackedRequestsUrl,
83                                {credentials: 'omit', mode: 'cors'});
84     let trackerData = await response.json();
86     // Fail on fetch error.
87     if (trackerData.error) {
88       throw trackedRequestsUrl + ' fetch failed:' +
89           JSON.stringify(trackerData);
90     }
92     // Fail on errors reported by the tracker script.
93     if (trackerData.errors.length > 0) {
94       throw 'Errors reported by request_tracker.py:' +
95           JSON.stringify(trackerData.errors);
96     }
98     // If expected number of requests have been observed, compare with list of
99     // all expected requests and exit.
100     let trackedRequests = trackerData.trackedRequests;
101     if (trackedRequests.length == expectedRequests.length) {
102       assert_array_equals(trackedRequests.sort(), expectedRequests);
103       break;
104     }
106     // If fewer than total number of expected requests have been observed,
107     // compare what's been received so far, to have a greater chance to fail
108     // rather than hang on error.
109     for (const trackedRequest of trackedRequests) {
110       assert_in_array(trackedRequest, expectedRequests);
111     }
112   }
115 // Creates a bidding script with the provided code in the method bodies. The
116 // bidding script's generateBid() method will return a bid of 9 for the first
117 // ad, after the passed in code in the "generateBid" input argument has been
118 // run, unless it returns something or throws.
120 // The default reportWin() method is empty.
121 function createBiddingScriptUrl(params = {}) {
122   let url = new URL(`${BASE_URL}resources/bidding-logic.sub.py`);
123   if (params.generateBid)
124     url.searchParams.append('generateBid', params.generateBid);
125   if (params.reportWin)
126     url.searchParams.append('reportWin', params.reportWin);
127   if (params.error)
128     url.searchParams.append('error', params.error);
129   if (params.bid)
130     url.searchParams.append('bid', params.bid);
131   return url.toString();
134 // Creates a decision script with the provided code in the method bodies. The
135 // decision script's scoreAd() method will reject ads with renderUrls that
136 // don't ends with "uuid", and will return a score equal to the bid, after the
137 // passed in code in the "scoreAd" input argument has been run, unless it
138 // returns something or throws.
140 // The default reportResult() method is empty.
141 function createDecisionScriptUrl(uuid, params = {}) {
142   let url = new URL(`${BASE_URL}resources/decision-logic.sub.py`);
143   url.searchParams.append('uuid', uuid);
144   if (params.scoreAd)
145     url.searchParams.append('scoreAd', params.scoreAd);
146   if (params.reportResult)
147     url.searchParams.append('reportResult', params.reportResult);
148   if (params.error)
149     url.searchParams.append('error', params.error);
150   return url.toString();
153 // Creates a renderUrl for an ad that runs the passed in "script". "uuid" has
154 // no effect, beyond making the URL distinct between tests, and being verified
155 // by the decision logic script before accepting a bid. "uuid" is expected to
156 // be last.
157 function createRenderUrl(uuid, script) {
158   let url = new URL(`${BASE_URL}resources/fenced-frame.sub.py`);
159   if (script)
160     url.searchParams.append('script', script);
161   url.searchParams.append('uuid', uuid);
162   return url.toString();
165 // Joins an interest group that, by default, is owned by the current frame's
166 // origin, is named DEFAULT_INTEREST_GROUP_NAME, has a bidding script that
167 // issues a bid of 9 with a renderUrl of "https://not.checked.test/${uuid}",
168 // and sends a report to createBidderReportUrl(uuid) if it wins. Waits for the
169 // join command to complete. Adds cleanup command to `test` to leave the
170 // interest group when the test completes.
172 // `interestGroupOverrides` may be used to override fields in the joined
173 // interest group.
174 async function joinInterestGroup(test, uuid, interestGroupOverrides = {}) {
175   const INTEREST_GROUP_LIFETIME_SECS = 60;
177   let interestGroup = {
178     owner: window.location.origin,
179     name: DEFAULT_INTEREST_GROUP_NAME,
180     biddingLogicUrl: createBiddingScriptUrl(
181         { reportWin: `sendReportTo('${createBidderReportUrl(uuid)}');` }),
182     ads: [{renderUrl: createRenderUrl(uuid)}],
183     ...interestGroupOverrides
184   };
186   await navigator.joinAdInterestGroup(interestGroup,
187                                       INTEREST_GROUP_LIFETIME_SECS);
188   test.add_cleanup(
189       async () => {await navigator.leaveAdInterestGroup(interestGroup)});
192 // Similar to joinInterestGroup, but leaves the interest group instead.
193 // Generally does not need to be called manually when using
194 // "joinInterestGroup()".
195 async function leaveInterestGroup(interestGroupOverrides = {}) {
196   let interestGroup = {
197     owner: window.location.origin,
198     name: DEFAULT_INTEREST_GROUP_NAME,
199     ...interestGroupOverrides
200   };
202   await navigator.leaveAdInterestGroup(interestGroup);
205 // Runs a FLEDGE auction and returns the result. By default, the seller is the
206 // current frame's origin, and the only buyer is as well. The seller script
207 // rejects bids for URLs that don't contain "uuid" (to avoid running into issues
208 // with any interest groups from other tests), and reportResult() sends a report
209 // to createSellerReportUrl(uuid).
211 // `auctionConfigOverrides` may be used to override fields in the auction
212 // configuration.
213 async function runBasicFledgeAuction(test, uuid, auctionConfigOverrides = {}) {
214   let auctionConfig = {
215     seller: window.location.origin,
216     decisionLogicUrl: createDecisionScriptUrl(
217         uuid,
218         { reportResult: `sendReportTo('${createSellerReportUrl(uuid)}');` }),
219     interestGroupBuyers: [window.location.origin],
220     ...auctionConfigOverrides
221   };
222   return await navigator.runAdAuction(auctionConfig);
225 // Calls runBasicFledgeAuction(), expecting the auction to have a winner.
226 // Creates a fenced frame that will be destroyed on completion of "test", and
227 // navigates it to the URN URL returned by the auction. Does not wait for the
228 // fenced frame to finish loading, since there's no API that can do that.
229 async function runBasicFledgeAuctionAndNavigate(test, uuid,
230                                                 auctionConfigOverrides = {}) {
231   let url = await runBasicFledgeAuction(test, uuid, auctionConfigOverrides);
232   assert_equals(typeof url, 'string',
233                 `Wrong value type returned from auction: ${typeof url}`);
235   let fencedFrame = document.createElement('fencedframe');
236   fencedFrame.mode = 'opaque-ads';
237   fencedFrame.src = url;
238   document.body.appendChild(fencedFrame);
239   test.add_cleanup(() => { document.body.removeChild(fencedFrame); });
242 // Joins an interest group and runs an auction, expecting a winner to be
243 // returned. "testConfig" can optionally modify the interest group or
244 // auctionConfig.
245 async function runBasicFledgeTestExpectingWinner(test, testConfig = {}) {
246   const uuid = generateUuid(test);
247   await joinInterestGroup(test, uuid, testConfig.interestGroupOverrides);
248   let url = await runBasicFledgeAuction(
249       test, uuid, testConfig.auctionConfigOverrides);
250   assert_equals(typeof url, 'string',
251       `Wrong value type returned from auction: ${typeof url}`);
254 // Joins an interest group and runs an auction, expecting no winner to be
255 // returned. "testConfig" can optionally modify the interest group or
256 // auctionConfig.
257 async function runBasicFledgeTestExpectingNoWinner(test, testConfig = {}) {
258   const uuid = generateUuid(test);
259   await joinInterestGroup(test, uuid, testConfig.interestGroupOverrides);
260   let result = await runBasicFledgeAuction(
261       test, uuid, testConfig.auctionConfigOverrides);
262   assert_true(result === null, 'Auction unexpectedly had a winner');
265 // Test helper for report phase of auctions that lets the caller specify the
266 // body of reportResult() and reportWin(). Passing in null will cause there
267 // to be no reportResult() or reportWin() method.
269 // If the "SuccessCondition" fields are non-null and evaluate to false in
270 // the corresponding reporting method, the report is sent to an error URL.
271 // Otherwise, the corresponding 'reportResult' / 'reportWin' values are run.
273 // `renderUrlOverride` allows the ad URL of the joined InterestGroup to
274 // to be set by the caller.
276 // Requesting error report URLs causes waitForObservedRequests() to throw
277 // rather than hang.
278 async function runReportTest(test, uuid, reportResultSuccessCondition,
279                              reportResult, reportWinSuccessCondition, reportWin,
280                              expectedReportUrls, renderUrlOverride) {
281   if (reportResultSuccessCondition) {
282     reportResult = `if (!(${reportResultSuccessCondition})) {
283                       sendReportTo('${createSellerReportUrl(uuid, 'error')}');
284                       return false;
285                     }
286                     ${reportResult}`;
287   }
288   let decisionScriptUrlParams = {};
289   if (reportResult !== null)
290     decisionScriptUrlParams.reportResult = reportResult;
291   else
292     decisionScriptUrlParams.error = 'no-reportResult';
294   if (reportWinSuccessCondition) {
295     reportWin = `if (!(${reportWinSuccessCondition})) {
296                    sendReportTo('${createSellerReportUrl(uuid, 'error')}');
297                    return false;
298                  }
299                  ${reportWin}`;
300   }
301   let biddingScriptUrlParams = {};
302   if (reportWin !== null)
303     biddingScriptUrlParams.reportWin = reportWin;
304   else
305     biddingScriptUrlParams.error = 'no-reportWin';
307   let interestGroupOverrides =
308       { biddingLogicUrl: createBiddingScriptUrl(biddingScriptUrlParams) };
309   if (renderUrlOverride)
310     interestGroupOverrides.ads = [{renderUrl: renderUrlOverride}]
312   await joinInterestGroup(test, uuid, interestGroupOverrides);
313   await runBasicFledgeAuctionAndNavigate(
314       test, uuid,
315       { decisionLogicUrl: createDecisionScriptUrl(
316                               uuid, decisionScriptUrlParams) });
317   await waitForObservedRequests(uuid, expectedReportUrls);