summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/fledge/tentative/resources
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 19:33:14 +0000
commit36d22d82aa202bb199967e9512281e9a53db42c9 (patch)
tree105e8c98ddea1c1e4784a60a5a6410fa416be2de /testing/web-platform/tests/fledge/tentative/resources
parentInitial commit. (diff)
downloadfirefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz
firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/fledge/tentative/resources')
-rw-r--r--testing/web-platform/tests/fledge/tentative/resources/bidding-logic.sub.py57
-rw-r--r--testing/web-platform/tests/fledge/tentative/resources/decision-logic.sub.py54
-rw-r--r--testing/web-platform/tests/fledge/tentative/resources/empty.html1
-rw-r--r--testing/web-platform/tests/fledge/tentative/resources/fenced-frame.sub.py19
-rw-r--r--testing/web-platform/tests/fledge/tentative/resources/fledge-util.js319
-rw-r--r--testing/web-platform/tests/fledge/tentative/resources/request-tracker.py97
-rw-r--r--testing/web-platform/tests/fledge/tentative/resources/trusted-bidding-signals.py124
7 files changed, 671 insertions, 0 deletions
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 @@
+<!DOCTYPE html>
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 """
+ <!DOCTYPE html>
+ <html>
+ <body>
+ <script>
+ {{GET[script]}}
+ </script>
+ </body>
+ </html>
+ """
+
+
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 "<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);
+}
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
+# "<url>, body: <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