diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:13:33 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 01:13:33 +0000 |
commit | 086c044dc34dfc0f74fbe41f4ecb402b2cd34884 (patch) | |
tree | a4f824bd33cb075dd5aa3eb5a0a94af221bbe83a /testing/web-platform/tests/fledge/tentative/resources | |
parent | Adding debian version 124.0.1-1. (diff) | |
download | firefox-086c044dc34dfc0f74fbe41f4ecb402b2cd34884.tar.xz firefox-086c044dc34dfc0f74fbe41f4ecb402b2cd34884.zip |
Merging upstream version 125.0.1.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/fledge/tentative/resources')
7 files changed, 298 insertions, 76 deletions
diff --git a/testing/web-platform/tests/fledge/tentative/resources/additional-bids.py b/testing/web-platform/tests/fledge/tentative/resources/additional-bids.py new file mode 100644 index 0000000000..060606b41d --- /dev/null +++ b/testing/web-platform/tests/fledge/tentative/resources/additional-bids.py @@ -0,0 +1,59 @@ +"""Endpoint to return additional bids in the appropriate response header. + +Additional bids are returned using the "Ad-Auction-Additional-Bid" response +header, as described at +https://github.com/WICG/turtledove/blob/main/FLEDGE.md#63-http-response-headers. + +This script generates one of "Ad-Auction-Additional-Bid" response header for +each additional bid provided in a url-encoded `additionalBids` query parameter. + +All requests to this endpoint requires a "Sec-Ad-Auction-Fetch" request header +with a value of b"?1"; this entrypoint otherwise returns a 400 response. +""" +import json +import base64 + +import fledge.tentative.resources.fledge_http_server_util as fledge_http_server_util + + +class BadRequestError(Exception): + pass + + +def main(request, response): + try: + if fledge_http_server_util.handle_cors_headers_and_preflight(request, response): + return + + # Verify that Sec-Ad-Auction-Fetch is present + if (request.headers.get("Sec-Ad-Auction-Fetch", default=b"").decode("utf-8") != "?1"): + raise BadRequestError("Sec-Ad-Auction-Fetch missing or unexpected value; expected '?1'") + + # Return each signed additional bid in its own header + additional_bids = request.GET.get(b"additionalBids", default=b"").decode("utf-8") + if not additional_bids: + raise BadRequestError("Missing 'additionalBids' parameter") + for additional_bid in json.loads(additional_bids): + additional_bid_string = json.dumps(additional_bid) + auction_nonce = additional_bid.get("auctionNonce", None) + if not auction_nonce: + raise BadRequestError("Additional bid missing required 'auctionNonce' field") + signed_additional_bid = json.dumps({ + "bid": additional_bid_string, + "signatures": [] + }) + additional_bid_header_value = (auction_nonce.encode("utf-8") + b":" + + base64.b64encode(signed_additional_bid.encode("utf-8"))) + response.headers.append(b"Ad-Auction-Additional-Bid", additional_bid_header_value) + + response.status = (200, b"OK") + response.headers.set(b"Content-Type", b"text/plain") + + except BadRequestError as error: + response.status = (400, b"Bad Request") + response.headers.set(b"Content-Type", b"text/plain") + response.content = str(error) + + except Exception as exception: + response.status = (500, b"Internal Server Error") + response.content = str(exception) diff --git a/testing/web-platform/tests/fledge/tentative/resources/bidding-logic.sub.py b/testing/web-platform/tests/fledge/tentative/resources/bidding-logic.sub.py index 707e37f36b..e17f2c2c75 100644 --- a/testing/web-platform/tests/fledge/tentative/resources/bidding-logic.sub.py +++ b/testing/web-platform/tests/fledge/tentative/resources/bidding-logic.sub.py @@ -1,71 +1,83 @@ from pathlib import Path +from fledge.tentative.resources import fledge_http_server_util + # General bidding logic script. Depending on query parameters, it can -# simulate a variety of network errors, and its generateBid() and -# reportWin() functions can have arbitrary Javascript code injected -# in them. generateBid() will by default return a bid of 9 for the +# simulate a variety of network errors, and its generateBid(), reportWin(), +# and reportAdditionalBidWin() functions can have arbitrary Javascript code +# injected in them. generateBid() will by default return a bid of 9 for the # first ad. def main(request, response): - error = request.GET.first(b"error", None) + if fledge_http_server_util.handle_cors_headers_and_preflight(request, response): + return + + error = request.GET.first(b"error", None) - if error == b"close-connection": - # Close connection without writing anything, to simulate a network - # error. The write call is needed to avoid writing the default headers. - response.writer.write("") - response.close_connection = True - return + if error == b"close-connection": + # Close connection without writing anything, to simulate a network + # error. The write call is needed to avoid writing the default headers. + response.writer.write("") + response.close_connection = True + return - if error == b"http-error": - response.status = (404, b"OK") - else: - response.status = (200, b"OK") + if error == b"http-error": + response.status = (404, b"OK") + else: + response.status = (200, b"OK") - if error == b"wrong-content-type": - response.headers.set(b"Content-Type", b"application/json") - elif error != b"no-content-type": - response.headers.set(b"Content-Type", b"application/javascript") + if error == b"wrong-content-type": + response.headers.set(b"Content-Type", b"application/json") + elif error != b"no-content-type": + response.headers.set(b"Content-Type", b"application/javascript") - if error == b"bad-allow-fledge": - response.headers.set(b"Ad-Auction-Allowed", b"sometimes") - elif error == b"fledge-not-allowed": - response.headers.set(b"Ad-Auction-Allowed", b"false") - elif error != b"no-allow-fledge": - response.headers.set(b"Ad-Auction-Allowed", b"true") + if error == b"bad-allow-fledge": + response.headers.set(b"Ad-Auction-Allowed", b"sometimes") + elif error == b"fledge-not-allowed": + response.headers.set(b"Ad-Auction-Allowed", b"false") + elif error != b"no-allow-fledge": + response.headers.set(b"Ad-Auction-Allowed", b"true") - if error == b"no-body": - return b'' + if error == b"no-body": + return b'' - body = (Path(__file__).parent.resolve() / 'worklet-helpers.js').read_text().encode("ASCII") - if error != b"no-generateBid": - # Use bid query param if present. Otherwise, use a bid of 9. - bid = (request.GET.first(b"bid", None) or b"9").decode("ASCII") + body = (Path(__file__).parent.resolve() / 'worklet-helpers.js').read_text().encode("ASCII") + if error != b"no-generateBid": + # Use bid query param if present. Otherwise, use a bid of 9. + bid = (request.GET.first(b"bid", None) or b"9").decode("ASCII") - bidCurrency = "" - bidCurrencyParam = request.GET.first(b"bidCurrency", None) - if bidCurrencyParam != None: - bidCurrency = "bidCurrency: '" + bidCurrencyParam.decode("ASCII") + "'," + bidCurrency = "" + bidCurrencyParam = request.GET.first(b"bidCurrency", None) + if bidCurrencyParam != None: + bidCurrency = "bidCurrency: '" + bidCurrencyParam.decode("ASCII") + "'," - allowComponentAuction = "" - allowComponentAuctionParam = request.GET.first(b"allowComponentAuction", None) - if allowComponentAuctionParam != None: - allowComponentAuction = f"allowComponentAuction: {allowComponentAuctionParam.decode('ASCII')}," + allowComponentAuction = "" + allowComponentAuctionParam = request.GET.first(b"allowComponentAuction", None) + if allowComponentAuctionParam != None: + allowComponentAuction = f"allowComponentAuction: {allowComponentAuctionParam.decode('ASCII')}," - body += f""" - function generateBid(interestGroup, auctionSignals, perBuyerSignals, - trustedBiddingSignals, browserSignals, - directFromSellerSignals) {{ - {{{{GET[generateBid]}}}}; - return {{ - bid: {bid}, - {bidCurrency} - {allowComponentAuction} - render: interestGroup.ads[0].renderURL - }}; - }}""".encode() - if error != b"no-reportWin": - body += b""" - function reportWin(auctionSignals, perBuyerSignals, sellerSignals, - browserSignals, directFromSellerSignals) { - {{GET[reportWin]}}; - }""" - return body + body += f""" + function generateBid(interestGroup, auctionSignals, perBuyerSignals, + trustedBiddingSignals, browserSignals, + directFromSellerSignals) {{ + {{{{GET[generateBid]}}}}; + return {{ + bid: {bid}, + {bidCurrency} + {allowComponentAuction} + render: interestGroup.ads[0].renderURL + }}; + }}""".encode() + if error != b"no-reportWin": + body += b""" + function reportWin(auctionSignals, perBuyerSignals, sellerSignals, + browserSignals, directFromSellerSignals) { + {{GET[reportWin]}}; + }""" + if error != b"no-reportAdditionalBidWin": + body += b""" + function reportAdditionalBidWin(auctionSignals, perBuyerSignals, + sellerSignals, browserSignals, + directFromSellerSignals) { + {{GET[reportAdditionalBidWin]}}; + }""" + return body diff --git a/testing/web-platform/tests/fledge/tentative/resources/decision-logic.sub.py b/testing/web-platform/tests/fledge/tentative/resources/decision-logic.sub.py index 78d459e3f9..3a23f98162 100644 --- a/testing/web-platform/tests/fledge/tentative/resources/decision-logic.sub.py +++ b/testing/web-platform/tests/fledge/tentative/resources/decision-logic.sub.py @@ -43,8 +43,8 @@ def main(request, response): // Don't bid on interest group with the wrong uuid. This is to prevent // left over interest groups from other tests from affecting auction // results. - if (!browserSignals.renderUrl.endsWith('uuid={{GET[uuid]}}') && - !browserSignals.renderUrl.includes('uuid={{GET[uuid]}}&')) { + if (!browserSignals.renderURL.endsWith('uuid={{GET[uuid]}}') && + !browserSignals.renderURL.includes('uuid={{GET[uuid]}}&')) { return 0; } diff --git a/testing/web-platform/tests/fledge/tentative/resources/fledge-util.sub.js b/testing/web-platform/tests/fledge/tentative/resources/fledge-util.sub.js index 69573d4998..5819357e29 100644 --- a/testing/web-platform/tests/fledge/tentative/resources/fledge-util.sub.js +++ b/testing/web-platform/tests/fledge/tentative/resources/fledge-util.sub.js @@ -33,7 +33,7 @@ const OTHER_ORIGIN7 = 'https://{{hosts[alt][www]}}:{{ports[https][1]}}'; // 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}${BASE_PATH}resources/request-tracker.py`); + let url = new URL(`${origin}${RESOURCE_PATH}request-tracker.py`); let search = `uuid=${uuid}&dispatch=${dispatch}`; if (id) search += `&id=${id}`; @@ -59,6 +59,10 @@ 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) { @@ -69,7 +73,7 @@ function createSellerBeaconURL(uuid, id = '1', origin = window.location.origin) } function createDirectFromSellerSignalsURL(origin = window.location.origin) { - let url = new URL(`${origin}${BASE_PATH}resources/direct-from-seller-signals.py`); + let url = new URL(`${origin}${RESOURCE_PATH}direct-from-seller-signals.py`); return url.toString(); } @@ -80,7 +84,7 @@ function generateUuid(test) { let uuid = token(); test.add_cleanup(async () => { let response = await fetch(createCleanupURL(uuid), - {credentials: 'omit', mode: 'cors'}); + { credentials: 'omit', mode: 'cors' }); assert_equals(await response.text(), 'cleanup complete', `Sever state cleanup failed`); }); @@ -94,7 +98,7 @@ async function fetchTrackedData(uuid) { let trackedRequestsURL = createTrackerURL(window.location.origin, uuid, 'tracked_data'); let response = await fetch(trackedRequestsURL, - {credentials: 'omit', mode: 'cors'}); + { credentials: 'omit', mode: 'cors' }); let trackedData = await response.json(); // Fail on fetch error. @@ -118,23 +122,29 @@ async function fetchTrackedData(uuid) { // 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) { +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.sort().map((url) => url.replace(uuid, '<uuid>')); + 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.sort().map( - (url) => url.replace(uuid, '<uuid>')); + let trackedRequests = trackedData.trackedRequests.map( + (url) => url.replace(uuid, '<uuid>')).sort(); + + if (filter) { + trackedRequests = trackedRequests.filter(filter); + } // If expected number of requests have been observed, compare with list of // all expected requests and exit. - if (trackedRequests.length == expectedRequests.length) { + if (trackedRequests.length >= expectedRequests.length) { assert_array_equals(trackedRequests, expectedRequests); break; } @@ -148,6 +158,16 @@ async function waitForObservedRequests(uuid, expectedRequests) { } } + +// 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 @@ -163,6 +183,8 @@ function createBiddingScriptURL(params = {}) { 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) @@ -254,7 +276,7 @@ async function joinInterestGroup(test, uuid, interestGroupOverrides = {}, await navigator.joinAdInterestGroup(interestGroup, durationSeconds); test.add_cleanup( - async () => {await navigator.leaveAdInterestGroup(interestGroup)}); + async () => { await navigator.leaveAdInterestGroup(interestGroup) }); } // Similar to joinInterestGroup, but leaves the interest group instead. @@ -465,6 +487,24 @@ async function runReportTest(test, uuid, codeToInsert, expectedReportURLs, await waitForObservedRequests(uuid, expectedReportURLs); } +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)]); +} + // 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 @@ -504,7 +544,7 @@ async function runInFrame(test, child_window, script, param) { // iframe or closes the window. async function createFrame(test, origin, is_iframe = true, permissions = null) { const frameUuid = generateUuid(test); - const frameUrl = + const frameURL = `${origin}${RESOURCE_PATH}subordinate-frame.sub.html?uuid=${frameUuid}`; let promise = new Promise(function(resolve, reject) { function WaitForMessage(event) { @@ -523,7 +563,7 @@ async function createFrame(test, origin, is_iframe = true, permissions = null) { let iframe = document.createElement('iframe'); if (permissions) iframe.allow = permissions; - iframe.src = frameUrl; + iframe.src = frameURL; document.body.appendChild(iframe); test.add_cleanup(async () => { @@ -535,7 +575,7 @@ async function createFrame(test, origin, is_iframe = true, permissions = null) { return iframe.contentWindow; } - let child_window = window.open(frameUrl); + let child_window = window.open(frameURL); test.add_cleanup(async () => { await runInFrame(test, child_window, "await test_instance.do_cleanup();"); child_window.close(); @@ -576,11 +616,23 @@ async function joinInterestGroupInTopLevelWindow( let interestGroup = JSON.stringify( createInterestGroupForOrigin(uuid, origin, interestGroupOverrides)); - let topLeveWindow = await createTopLevelWindow(test, origin); - await runInFrame(test, topLeveWindow, + 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) { @@ -643,3 +695,46 @@ function directFromSellerSignalsValidatorCode(uuid, expectedSellerSignals, `sendReportTo("${createBidderReportURL(uuid)}");`, }; } + +// 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. +function createAdditionalBid(uuid, auctionNonce, seller, buyer, interestGroupName, bidAmount, + additionalBidOverrides = {}) { + return { + interestGroup: { + name: interestGroupName, + biddingLogicURL: createBiddingScriptURL( + { + origin: buyer, + reportAdditionalBidWin: `sendReportTo("${createBidderReportURL(uuid, interestGroupName)}");` + }), + owner: buyer + }, + bid: { + ad: ['metadata'], + bid: bidAmount, + render: createRenderURL(uuid) + }, + auctionNonce: auctionNonce, + seller: seller, + ...additionalBidOverrides + } +} + +// Fetch some number of 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. +async function fetchAdditionalBids(seller, additionalBids) { + const url = new URL(`${seller}${RESOURCE_PATH}additional-bids.py`); + url.searchParams.append('additionalBids', JSON.stringify(additionalBids)); + 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.'); +} diff --git a/testing/web-platform/tests/fledge/tentative/resources/permissions.py b/testing/web-platform/tests/fledge/tentative/resources/permissions.py new file mode 100644 index 0000000000..eed93c4275 --- /dev/null +++ b/testing/web-platform/tests/fledge/tentative/resources/permissions.py @@ -0,0 +1,54 @@ +"""Methods for the interest group cross-origin permissions endpoint.""" +import json +import re + +from fledge.tentative.resources import fledge_http_server_util + +SUBDOMAIN_WWW = 'www' +SUBDOMAIN_WWW1 = 'www1' +SUBDOMAIN_WWW2 = 'www2' +SUBDOMAIN_FRENCH = 'élève'.encode('idna').decode() +SUBDOMAIN_JAPANESE = '天気の良い日'.encode('idna').decode() +ALL_SUBDOMAINS = [SUBDOMAIN_WWW, SUBDOMAIN_WWW1, SUBDOMAIN_WWW2, + SUBDOMAIN_FRENCH, SUBDOMAIN_JAPANESE] + +def get_permissions(request, response): + """Returns JSON object containing interest group cross-origin permissions. + + The structure returned is described in more detail at + https://github.com/WICG/turtledove/blob/main/FLEDGE.md#13-permission-delegation. + This correctly handles requests issued in CORS mode. + + This .well-known is fetched at the origin of the interest group's owner, and + specifies as a URL parameter the origin of frame that's attempting to join or + leave that interest group. + + This is implemented such that the origin of the frame is ignored altogether, + and the determination of which operations are allowed depends strictly on the + origin of the interest group owner, and specifically on the subdomain of the + origin of the interest group owner. wptserve serves each of its two domains + at both the raw domain and each of five subdomains. + + - www: disallows both join and leave + - www1: allows join, but not leave + - www2: allows leave, but not join + - 天気の良い日 / élève: allow both join and leave + - anything else (including no subdomain): returns a 404 + """ + if fledge_http_server_util.handle_cors_headers_and_preflight(request, response): + return + + first_domain_label = re.search(r"[^.]*", request.url_parts.netloc).group(0) + if first_domain_label not in ALL_SUBDOMAINS: + response.status = (404, b"Not Found") + response.content = "Not Found" + return + + response.status = (200, b"OK") + response.headers.set(b"Content-Type", b"application/json") + response.content = json.dumps({ + "joinAdInterestGroup": first_domain_label in [ + SUBDOMAIN_WWW1, SUBDOMAIN_FRENCH, SUBDOMAIN_JAPANESE], + "leaveAdInterestGroup": first_domain_label in [ + SUBDOMAIN_WWW2, SUBDOMAIN_FRENCH, SUBDOMAIN_JAPANESE], + }) diff --git a/testing/web-platform/tests/fledge/tentative/resources/request-tracker.py b/testing/web-platform/tests/fledge/tentative/resources/request-tracker.py index c449d2ab02..3514741f63 100644 --- a/testing/web-platform/tests/fledge/tentative/resources/request-tracker.py +++ b/testing/web-platform/tests/fledge/tentative/resources/request-tracker.py @@ -110,4 +110,6 @@ def simple_response(request, response, status_code, status_message, body, content_type=b"text/plain"): response.status = (status_code, status_message) response.headers.set(b"Content-Type", content_type) + # Force refetch on reuse, so multiple requests to tracked URLs are all visible. + response.headers.set(b"Cache-control", b"no-store") return body diff --git a/testing/web-platform/tests/fledge/tentative/resources/trusted-scoring-signals.py b/testing/web-platform/tests/fledge/tentative/resources/trusted-scoring-signals.py index 80488a5d6a..eccef5e762 100644 --- a/testing/web-platform/tests/fledge/tentative/resources/trusted-scoring-signals.py +++ b/testing/web-platform/tests/fledge/tentative/resources/trusted-scoring-signals.py @@ -15,7 +15,7 @@ def main(request, response): renderUrls = None adComponentRenderURLs = None # List of {type: <render URL type>, urls: <render URL list>} pairs, where <render URL type> is - # one of the two render URL dictionary keys used in the response ("renderUrls" or + # one of the two render URL dictionary keys used in the response ("renderURLs" or # "adComponentRenderURLs"). May be of length 1 or 2, depending on whether there # are any component URLs. urlLists = [] @@ -36,7 +36,7 @@ def main(request, response): continue if pair[0] == "renderUrls" and renderUrls == None: renderUrls = list(map(unquote_plus, pair[1].split(","))) - urlLists.append({"type":"renderUrls", "urls":renderUrls}) + urlLists.append({"type":"renderURLs", "urls":renderUrls}) continue if pair[0] == "adComponentRenderUrls" and adComponentRenderURLs == None: adComponentRenderURLs = list(map(unquote_plus, pair[1].split(","))) |