From 36d22d82aa202bb199967e9512281e9a53db42c9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 21:33:14 +0200 Subject: Adding upstream version 115.7.0esr. Signed-off-by: Daniel Baumann --- .../tentative/resources/bidding-logic.sub.py | 57 ++++ .../tentative/resources/decision-logic.sub.py | 54 ++++ .../tests/fledge/tentative/resources/empty.html | 1 + .../fledge/tentative/resources/fenced-frame.sub.py | 19 ++ .../fledge/tentative/resources/fledge-util.js | 319 +++++++++++++++++++++ .../fledge/tentative/resources/request-tracker.py | 97 +++++++ .../tentative/resources/trusted-bidding-signals.py | 124 ++++++++ 7 files changed, 671 insertions(+) create mode 100644 testing/web-platform/tests/fledge/tentative/resources/bidding-logic.sub.py create mode 100644 testing/web-platform/tests/fledge/tentative/resources/decision-logic.sub.py create mode 100644 testing/web-platform/tests/fledge/tentative/resources/empty.html create mode 100644 testing/web-platform/tests/fledge/tentative/resources/fenced-frame.sub.py create mode 100644 testing/web-platform/tests/fledge/tentative/resources/fledge-util.js create mode 100644 testing/web-platform/tests/fledge/tentative/resources/request-tracker.py create mode 100644 testing/web-platform/tests/fledge/tentative/resources/trusted-bidding-signals.py (limited to 'testing/web-platform/tests/fledge/tentative/resources') 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 new file mode 100644 index 0000000000..6b4e179cff --- /dev/null +++ b/testing/web-platform/tests/fledge/tentative/resources/bidding-logic.sub.py @@ -0,0 +1,57 @@ +# 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 +# first ad. +def main(request, response): + 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"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"bad-allow-fledge": + response.headers.set(b"X-Allow-FLEDGE", b"sometimes") + elif error == b"fledge-not-allowed": + response.headers.set(b"X-Allow-FLEDGE", b"false") + elif error != b"no-allow-fledge": + response.headers.set(b"X-Allow-FLEDGE", b"true") + + body = b'' + if error == b"no-body": + return body + if error != b"no-generateBid": + body += b""" + function generateBid(interestGroup, auctionSignals, perBuyerSignals, + trustedBiddingSignals, browserSignals, + directFromSellerSignals) { + {{GET[generateBid]}}; + return { + 'bid': 9, + 'render': interestGroup.ads[0].renderUrl + }; + }""" + bid = request.GET.first(b"bid", None) + if bid != None: + body = body.replace(b"9", bid) + if error != b"no-reportWin": + body += b""" + function reportWin(auctionSignals, perBuyerSignals, sellerSignals, + browserSignals, directFromSellerSignals) { + {{GET[reportWin]}}; + }""" + 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 new file mode 100644 index 0000000000..9a97d45bd1 --- /dev/null +++ b/testing/web-platform/tests/fledge/tentative/resources/decision-logic.sub.py @@ -0,0 +1,54 @@ +# General decision logic script. Depending on query parameters, it can +# simulate a variety of network errors, and its scoreAd() and +# reportResult() functions can have arbitrary Javascript code injected +# in them. scoreAd() will by default return a desirability score of +# twice the bid for each ad, as long as the ad URL ends with the uuid. +def main(request, response): + 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"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"bad-allow-fledge": + response.headers.set(b"X-Allow-FLEDGE", b"sometimes") + elif error == b"fledge-not-allowed": + response.headers.set(b"X-Allow-FLEDGE", b"false") + elif error != b"no-allow-fledge": + response.headers.set(b"X-Allow-FLEDGE", b"true") + + body = b'' + if error == b"no-body": + return body + if error != b"no-scoreAd": + body += b""" + function scoreAd(adMetadata, bid, auctionConfig, trustedScoringSignals, + browserSignals) { + // 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('{{GET[uuid]}}')) + return 0; + + {{GET[scoreAd]}}; + return {desirability: 2 * bid, allowComponentAuction: true}; + }""" + if error != b"no-reportResult": + body += b""" + function reportResult(auctionConfig, browserSignals, directFromSellerSignals) { + {{GET[reportResult]}}; + }""" + return body diff --git a/testing/web-platform/tests/fledge/tentative/resources/empty.html b/testing/web-platform/tests/fledge/tentative/resources/empty.html new file mode 100644 index 0000000000..0e76edd65b --- /dev/null +++ b/testing/web-platform/tests/fledge/tentative/resources/empty.html @@ -0,0 +1 @@ + diff --git a/testing/web-platform/tests/fledge/tentative/resources/fenced-frame.sub.py b/testing/web-platform/tests/fledge/tentative/resources/fenced-frame.sub.py new file mode 100644 index 0000000000..c29bb6fecc --- /dev/null +++ b/testing/web-platform/tests/fledge/tentative/resources/fenced-frame.sub.py @@ -0,0 +1,19 @@ +# Fenced frame HTML body. Generated by a Python file to avoid having quotes in +# the injected script escaped, which the test server does to *.html files. +def main(request, response): + response.status = (200, b"OK") + response.headers.set(b"Content-Type", b"text/html") + response.headers.set(b"Supports-Loading-Mode", b"fenced-frame") + + return """ + + + + + + + """ + + diff --git a/testing/web-platform/tests/fledge/tentative/resources/fledge-util.js b/testing/web-platform/tests/fledge/tentative/resources/fledge-util.js new file mode 100644 index 0000000000..5a05764e74 --- /dev/null +++ b/testing/web-platform/tests/fledge/tentative/resources/fledge-util.js @@ -0,0 +1,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 ", 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); +} diff --git a/testing/web-platform/tests/fledge/tentative/resources/request-tracker.py b/testing/web-platform/tests/fledge/tentative/resources/request-tracker.py new file mode 100644 index 0000000000..46da796f30 --- /dev/null +++ b/testing/web-platform/tests/fledge/tentative/resources/request-tracker.py @@ -0,0 +1,97 @@ +import mimetypes +import os +import json +import wptserve.stash + +from wptserve.utils import isomorphic_decode, isomorphic_encode + +# Test server that tracks requests it has previously seen, keyed by a token. +# +# All requests have a "dispatch" field indicating what to do, and a "uuid" +# field which should be unique for each test, to avoid tests that fail to +# clean up after themselves, or that are running concurrently, from interfering +# with other tests. +# +# Each uuid has a stash entry with a dictionary with two entries: +# "trackedRequests" is a list of all observed requested URLs with a +# dispatch of "track_get" or "track_post". POSTS are in the format +# ", body: ". +# "errors" is a list of an errors that occurred. +# +# A dispatch of "request_list" will return the "trackedRequests" dictionary +# associated with the in uuid, as a JSON string. +# +# A dispatch of "clean_up" will delete all information associated with the uuid. +def main(request, response): + # Don't cache responses, since tests expect duplicate requests to always + # reach the server. + response.headers.set(b"Cache-Control", b"no-store") + + dispatch = request.GET.first(b"dispatch", None) + uuid = request.GET.first(b"uuid", None) + + if not uuid or not dispatch: + return simple_response(request, response, 404, b"Not found", + b"Invalid query parameters") + + stash = request.server.stash + with stash.lock: + # Take ownership of stashed entry, if any. This removes the entry of the + # stash. + server_state = stash.take(uuid) or {"trackedRequests": [], "errors": []} + + # Clear the entire stash. No work to do, since stash entry was already + # removed. + if dispatch == b"clean_up": + return simple_response(request, response, 200, b"OK", + b"cleanup complete") + + # Return the list of entries in the stash. Need to add data back to the + # stash first. + if dispatch == b"request_list": + stash.put(uuid, server_state) + return simple_response(request, response, 200, b"OK", + json.dumps(server_state)) + + # Tracks a request that's expected to be a GET. + if dispatch == b"track_get": + if request.method != "GET": + server_state["errors"].append( + request.url + " has wrong method: " + request.method) + else: + server_state["trackedRequests"].append(request.url) + + stash.put(uuid, server_state) + return simple_response(request, response, 200, b"OK", b"") + + # Tracks a request that's expected to be a POST. + # In addition to the method, check the Content-Type, which is currently + # always text/plain, and compare the body against the expected body. + if dispatch == b"track_post": + contentType = request.headers.get(b"Content-Type", b"missing") + if request.method != "POST": + server_state["errors"].append( + request.url + " has wrong method: " + request.method) + elif not contentType.startswith(b"text/plain"): + server_state["errors"].append( + request.url + " has wrong Content-Type: " + + contentType.decode("utf-8")) + else: + server_state["trackedRequests"].append( + request.url + ", body: " + request.body.decode("utf-8")) + stash.put(uuid, server_state) + return simple_response(request, response, 200, b"OK", b"") + + # Report unrecognized dispatch line. + server_state["errors"].append( + request.url + " request with unknown dispatch value received: " + + dispatch.decode("utf-8")) + stash.put(uuid, server_state) + return simple_response(request, response, 404, b"Not Found", + b"Unrecognized dispatch parameter: " + dispatch) + +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) + return body diff --git a/testing/web-platform/tests/fledge/tentative/resources/trusted-bidding-signals.py b/testing/web-platform/tests/fledge/tentative/resources/trusted-bidding-signals.py new file mode 100644 index 0000000000..cdd7052a96 --- /dev/null +++ b/testing/web-platform/tests/fledge/tentative/resources/trusted-bidding-signals.py @@ -0,0 +1,124 @@ +import json +from urllib.parse import unquote_plus + +# Script to generate trusted bidding signals. The responses depends on the +# keys and interestGroupNames - some result in entire response failures, others +# affect only their own value. Keys are preferentially used over +# interestGroupName, since keys are composible, but some tests need to cover +# there being no keys. +def main(request, response): + hostname = None + keys = None + interestGroupNames = None + + # Manually parse query params. Can't use request.GET because it unescapes as well as splitting, + # and commas mean very different things from escaped commas. + for param in request.url_parts.query.split("&"): + pair = param.split("=", 1) + if len(pair) != 2: + return fail(response, "Bad query parameter: " + param) + # Browsers should escape query params consistently. + if "%20" in pair[1]: + return fail(response, "Query parameter should escape using '+': " + param) + + # Hostname can't be empty. The empty string can be a key or interest group name, though. + if pair[0] == "hostname" and hostname == None and len(pair[1]) > 0: + hostname = pair[1] + continue + if pair[0] == "keys" and keys == None: + keys = list(map(unquote_plus, pair[1].split(","))) + continue + if pair[0] == "interestGroupNames" and interestGroupNames == None: + interestGroupNames = list(map(unquote_plus, pair[1].split(","))) + continue + return fail(response, "Unexpected query parameter: " + param) + + # "interestGroupNames" and "hostname" are mandatory. + if not hostname: + return fail(response, "hostname missing") + if not interestGroupNames: + return fail(response, "interestGroupNames missing") + + response.status = (200, b"OK") + + # The JSON representation of this is used as the response body. This does + # not currently include a "perInterestGroupData" object. + responseBody = {"keys": {}} + + # Set when certain special keys are observed, used in place of the JSON + # representation of `responseBody`, when set. + body = None + + contentType = "application/json" + xAllowFledge = "true" + dataVersion = None + if keys: + for key in keys: + value = "default value" + if key == "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 + elif key.startswith("replace-body:"): + # Replace entire response body. Continue to run through other + # keys, to allow them to modify request headers. + body = key.split(':', 1)[1] + elif key.startswith("data-version:"): + dataVersion = key.split(':', 1)[1] + elif key == "http-error": + response.status = (404, b"Not found") + elif key == "no-content-type": + contentType = None + elif key == "wrong-content-type": + contentType = 'text/plain' + elif key == "wrongContentType": + contentType = 'text/plain' + elif key == "bad-allow-fledge": + xAllowFledge = "sometimes" + elif key == "fledge-not-allowed": + xAllowFledge = "false" + elif key == "no-allow-fledge": + xAllowFledge = None + elif key == "no-value": + continue + elif key == "wrong-value": + responseBody["keys"]["another-value"] = "another-value" + continue + elif key == "null-value": + value = None + elif key == "num-value": + value = 1 + elif key == "string-value": + value = "1" + elif key == "array-value": + value = [1, "foo", None] + elif key == "object-value": + value = {"a":"b", "c":["d"]} + elif key == "interest-group-names": + value = json.dumps(interestGroupNames) + elif key == "hostname": + value = request.GET.first(b"hostname", b"not-found").decode("ASCII") + responseBody["keys"][key] = value + + if "data-version" in interestGroupNames: + dataVersion = "4" + + if contentType: + response.headers.set("Content-Type", contentType) + if xAllowFledge: + response.headers.set("X-Allow-FLEDGE", xAllowFledge) + if dataVersion: + response.headers.set("Data-Version", dataVersion) + response.headers.set("X-fledge-bidding-signals-format-version", "2") + + if body != None: + return body + return json.dumps(responseBody) + +def fail(response, body): + response.status = (400, "Bad Request") + response.headers.set(b"Content-Type", b"text/plain") + return body -- cgit v1.2.3