1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
|
"use strict;"
const FULL_URL = window.location.href;
const 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';
// Unlike other URLs, the trustedBiddingSignalsUrl can't have a query string
// that's set by tests, since FLEDGE controls it entirely, so tests that
// exercise it use a fixed URL string. Special keys and interest group names
// control the response.
const TRUSTED_BIDDING_SIGNALS_URL =
`${BASE_URL}resources/trusted-bidding-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.
function createTrackerUrl(origin, uuid, dispatch, id = null) {
let url = new URL(`${origin}${BASE_PATH}resources/request-tracker.py`);
url.searchParams.append('uuid', uuid);
url.searchParams.append('dispatch', dispatch);
if (id)
url.searchParams.append('id', id);
return url.toString();
}
// 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.
function createBidderReportUrl(uuid, id = '1') {
return createTrackerUrl(window.location.origin, uuid, `track_get`,
`bidder_report_${id}`);
}
function createSellerReportUrl(uuid, id = '1') {
return createTrackerUrl(window.location.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') {
return createTrackerUrl(window.location.origin, uuid, `track_post`,
`bidder_beacon_${id}`);
}
function createSellerBeaconUrl(uuid, id = '1') {
return createTrackerUrl(window.location.origin, uuid, `track_post`,
`seller_beacon_${id}`);
}
// 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 cleanupUrl = createTrackerUrl(window.location.origin, uuid, 'clean_up');
let response = await fetch(cleanupUrl, {credentials: 'omit', mode: 'cors'});
assert_equals(await response.text(), 'cleanup complete',
`Sever state cleanup failed`);
});
return uuid;
}
// Repeatedly requests "request_list" 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.
//
// 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) {
let trackedRequestsUrl = createTrackerUrl(window.location.origin, uuid,
'request_list');
// Sort array for easier comparison, since order doesn't matter.
expectedRequests.sort();
while (true) {
let response = await fetch(trackedRequestsUrl,
{credentials: 'omit', mode: 'cors'});
let trackerData = await response.json();
// Fail on fetch error.
if (trackerData.error) {
throw trackedRequestsUrl + ' fetch failed:' +
JSON.stringify(trackerData);
}
// Fail on errors reported by the tracker script.
if (trackerData.errors.length > 0) {
throw 'Errors reported by request-tracker.py:' +
JSON.stringify(trackerData.errors);
}
// If expected number of requests have been observed, compare with list of
// all expected requests and exit.
let trackedRequests = trackerData.trackedRequests;
if (trackedRequests.length == expectedRequests.length) {
assert_array_equals(trackedRequests.sort(), 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 url = new URL(`${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(`${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(`${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}",
// 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 = {}) {
const INTEREST_GROUP_LIFETIME_SECS = 60;
let interestGroup = {
owner: window.location.origin,
name: DEFAULT_INTEREST_GROUP_NAME,
biddingLogicUrl: createBiddingScriptUrl(
{ reportWin: `sendReportTo('${createBidderReportUrl(uuid)}');` }),
ads: [{renderUrl: createRenderUrl(uuid)}],
...interestGroupOverrides
};
await navigator.joinAdInterestGroup(interestGroup,
INTEREST_GROUP_LIFETIME_SECS);
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);
}
// 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 a winner to be
// returned. "testConfig" can optionally modify the interest group or
// auctionConfig.
async function runBasicFledgeTestExpectingWinner(test, testConfig = {}) {
const uuid = generateUuid(test);
await joinInterestGroup(test, uuid, testConfig.interestGroupOverrides);
let config = await runBasicFledgeAuction(
test, uuid, testConfig.auctionConfigOverrides);
assert_true(config instanceof FencedFrameConfig,
`Wrong value type returned from auction: ${config.constructor.type}`);
}
// 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 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.
//
// `renderUrlOverride` allows the ad URL of the joined InterestGroup to
// to be set by the caller.
//
// Requesting error report URLs causes waitForObservedRequests() to throw
// rather than hang.
async function runReportTest(test, uuid, reportResultSuccessCondition,
reportResult, reportWinSuccessCondition, reportWin,
expectedReportUrls, renderUrlOverride) {
if (reportResultSuccessCondition) {
reportResult = `if (!(${reportResultSuccessCondition})) {
sendReportTo('${createSellerReportUrl(uuid, 'error')}');
return false;
}
${reportResult}`;
}
let decisionScriptUrlParams = {};
if (reportResult !== null)
decisionScriptUrlParams.reportResult = reportResult;
else
decisionScriptUrlParams.error = 'no-reportResult';
if (reportWinSuccessCondition) {
reportWin = `if (!(${reportWinSuccessCondition})) {
sendReportTo('${createSellerReportUrl(uuid, 'error')}');
return false;
}
${reportWin}`;
}
let biddingScriptUrlParams = {};
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);
await runBasicFledgeAuctionAndNavigate(
test, uuid,
{ decisionLogicUrl: createDecisionScriptUrl(
uuid, decisionScriptUrlParams) });
await waitForObservedRequests(uuid, expectedReportUrls);
}
|