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);
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) {
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`);
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,
79 // Sort array for easier comparison, since order doesn't matter.
80 expectedRequests.sort();
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);
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);
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);
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);
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);
128 url.searchParams.append('error', params.error);
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);
145 url.searchParams.append('scoreAd', params.scoreAd);
146 if (params.reportResult)
147 url.searchParams.append('reportResult', params.reportResult);
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
157 function createRenderUrl(uuid, script) {
158 let url = new URL(`${BASE_URL}resources/fenced-frame.sub.py`);
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
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
186 await navigator.joinAdInterestGroup(interestGroup,
187 INTEREST_GROUP_LIFETIME_SECS);
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
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
213 async function runBasicFledgeAuction(test, uuid, auctionConfigOverrides = {}) {
214 let auctionConfig = {
215 seller: window.location.origin,
216 decisionLogicUrl: createDecisionScriptUrl(
218 { reportResult: `sendReportTo('${createSellerReportUrl(uuid)}');` }),
219 interestGroupBuyers: [window.location.origin],
220 ...auctionConfigOverrides
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
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
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
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')}');
288 let decisionScriptUrlParams = {};
289 if (reportResult !== null)
290 decisionScriptUrlParams.reportResult = reportResult;
292 decisionScriptUrlParams.error = 'no-reportResult';
294 if (reportWinSuccessCondition) {
295 reportWin = `if (!(${reportWinSuccessCondition})) {
296 sendReportTo('${createSellerReportUrl(uuid, 'error')}');
301 let biddingScriptUrlParams = {};
302 if (reportWin !== null)
303 biddingScriptUrlParams.reportWin = reportWin;
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(
315 { decisionLogicUrl: createDecisionScriptUrl(
316 uuid, decisionScriptUrlParams) });
317 await waitForObservedRequests(uuid, expectedReportUrls);