[FLEDGE]:  Add trustedScoringSignalsUrl WPT tests.

This doesn't test the prioritization vector or componentRenderUrl
fields, which we'll add tests for when we test those specific features.

Bug: 1425952
Change-Id: Ida3ce4d22dfe52a72cabfaeda90e77845142ef79
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4762089
Reviewed-by: Maks Orlovich <morlovich@chromium.org>
Commit-Queue: Matt Menke <mmenke@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1181551}
diff --git a/fledge/tentative/TODO b/fledge/tentative/TODO
index 96654d2..fc2c296 100644
--- a/fledge/tentative/TODO
+++ b/fledge/tentative/TODO
@@ -18,8 +18,7 @@
     loading URNs in fencedframes in other frames, loading component
     ad URNs in fenced frames of other frames, etc)
 * adAuctionConfig passed to reportResult().
-* trusted scoring signals.
-* Component ads.
+* Component ads (including scoring signals fetches).
 * Component auctions.
 * browserSignals fields in scoring/bidding methods.
 * In reporting methods, browserSignals fields: dataVersion, topLevelSeller,
diff --git a/fledge/tentative/resources/fledge-util.js b/fledge/tentative/resources/fledge-util.js
index 7fef79c..f941d22 100644
--- a/fledge/tentative/resources/fledge-util.js
+++ b/fledge/tentative/resources/fledge-util.js
@@ -6,12 +6,15 @@
 
 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.
+// Unlike other URLs, trusted signals URLs can't have query strings
+// that are set by tests, since FLEDGE controls it entirely, so tests that
+// exercise them use a fixed URL string. Note that FLEDGE adds query
+// params when requesting these URLs, and the python scripts use these
+// to construct the response.
 const TRUSTED_BIDDING_SIGNALS_URL =
     `${BASE_URL}resources/trusted-bidding-signals.py`;
+const TRUSTED_SCORING_SIGNALS_URL =
+    `${BASE_URL}resources/trusted-scoring-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.
@@ -159,11 +162,14 @@
 // 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) {
+// be last.  "signalsParams" also has no effect, but is used by
+// trusted-scoring-signals.py to affect the response.
+function createRenderUrl(uuid, script, signalsParams) {
   let url = new URL(`${BASE_URL}resources/fenced-frame.sub.py`);
   if (script)
     url.searchParams.append('script', script);
+  if (signalsParams)
+    url.searchParams.append('signalsParams', signalsParams);
   url.searchParams.append('uuid', uuid);
   return url.toString();
 }
@@ -245,10 +251,10 @@
 }
 
 // Joins an interest group and runs an auction, expecting a winner to be
-// returned. "testConfig" can optionally modify the interest group or
+// returned. "testConfig" can optionally modify the uuid, interest group or
 // auctionConfig.
 async function runBasicFledgeTestExpectingWinner(test, testConfig = {}) {
-  const uuid = generateUuid(test);
+  const uuid = testConfig.uuid ? testConfig.uuid : generateUuid(test);
   await joinInterestGroup(test, uuid, testConfig.interestGroupOverrides);
   let config = await runBasicFledgeAuction(
       test, uuid, testConfig.auctionConfigOverrides);
@@ -257,10 +263,10 @@
 }
 
 // Joins an interest group and runs an auction, expecting no winner to be
-// returned. "testConfig" can optionally modify the interest group or
+// returned. "testConfig" can optionally modify the uuid, interest group or
 // auctionConfig.
 async function runBasicFledgeTestExpectingNoWinner(test, testConfig = {}) {
-  const uuid = generateUuid(test);
+  const uuid = testConfig.uuid ? testConfig.uuid : generateUuid(test);
   await joinInterestGroup(test, uuid, testConfig.interestGroupOverrides);
   let result = await runBasicFledgeAuction(
       test, uuid, testConfig.auctionConfigOverrides);
diff --git a/fledge/tentative/resources/trusted-scoring-signals.py b/fledge/tentative/resources/trusted-scoring-signals.py
new file mode 100644
index 0000000..6036d2e
--- /dev/null
+++ b/fledge/tentative/resources/trusted-scoring-signals.py
@@ -0,0 +1,127 @@
+import json
+from urllib.parse import unquote_plus, urlparse
+
+# Script to generate trusted scoring signals. The responses depends on the
+# query strings in the ads Urls - some result in entire response failures,
+# others affect only their own value. Each renderUrl potentially has a
+# signalsParam, which is a comma-delimited list of instructions that can
+# each affect either the value associated with the renderUrl, or the
+# response as a whole.
+def main(request, response):
+    hostname = None
+    renderUrls = None
+    adComponentRenderUrls = 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] == "renderUrls" and renderUrls == None:
+            renderUrls = list(map(unquote_plus, pair[1].split(",")))
+            continue
+        if pair[0] == "adComponentRenderUrls" and adComponentRenderUrls == None:
+            adComponentRenderUrls = list(map(unquote_plus, pair[1].split(",")))
+            continue
+        return fail(response, "Unexpected query parameter: " + param)
+
+    # "hostname" and "renderUrls" are mandatory.
+    if not hostname:
+        return fail(response, "hostname missing")
+    if not renderUrls:
+        return fail(response, "renderUrls missing")
+
+    response.status = (200, b"OK")
+
+    # The JSON representation of this is used as the response body.
+    responseBody = {"renderUrls": {}}
+
+    # Set when certain special keys are observed, used in place of the JSON
+    # representation of `responseBody`, when set.
+    body = None
+
+    contentType = "application/json"
+    adAuctionAllowed = "true"
+    dataVersion = None
+    if renderUrls:
+        for renderUrl in renderUrls:
+            value = "default value"
+
+            signalsParams = None
+            for param in urlparse(renderUrl).query.split("&"):
+                pair = param.split("=", 1)
+                if len(pair) != 2:
+                    continue
+                if pair[0] == "signalsParams":
+                    if signalsParams != None:
+                        return fail(response, "renderUrl has multiple signalsParams: " + renderUrl)
+                    signalsParams = pair[1]
+            if signalsParams != None:
+                signalsParams = unquote_plus(signalsParams)
+                for signalsParam in signalsParams.split(","):
+                    if signalsParam == "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 signalsParam.startswith("replace-body:"):
+                        # Replace entire response body. Continue to run through other
+                        # renderUrls, to allow them to modify request headers.
+                        body = signalsParam.split(':', 1)[1]
+                    elif signalsParam.startswith("data-version:"):
+                        dataVersion = signalsParam.split(':', 1)[1]
+                    elif signalsParam == "http-error":
+                        response.status = (404, b"Not found")
+                    elif signalsParam == "no-content-type":
+                        contentType = None
+                    elif signalsParam == "wrong-content-type":
+                        contentType = 'text/plain'
+                    elif signalsParam == "bad-ad-auction-allowed":
+                        adAuctionAllowed = "sometimes"
+                    elif signalsParam == "ad-auction-not-allowed":
+                        adAuctionAllowed = "false"
+                    elif signalsParam == "no-ad-auction-allow":
+                        adAuctionAllowed = None
+                    elif signalsParam == "no-value":
+                        continue
+                    elif signalsParam == "null-value":
+                        value = None
+                    elif signalsParam == "num-value":
+                        value = 1
+                    elif signalsParam == "string-value":
+                        value = "1"
+                    elif signalsParam == "array-value":
+                        value = [1, "foo", None]
+                    elif signalsParam == "object-value":
+                        value = {"a":"b", "c":["d"]}
+                    elif signalsParam == "hostname":
+                        value = request.GET.first(b"hostname", b"not-found").decode("ASCII")
+            if value != None:
+                responseBody["renderUrls"][renderUrl] = value
+
+    if contentType:
+        response.headers.set("Content-Type", contentType)
+    if adAuctionAllowed:
+        response.headers.set("Ad-Auction-Allowed", adAuctionAllowed)
+    if dataVersion:
+        response.headers.set("Data-Version", dataVersion)
+
+    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
diff --git a/fledge/tentative/trusted-scoring-signals.https.sub.window.js b/fledge/tentative/trusted-scoring-signals.https.sub.window.js
new file mode 100644
index 0000000..660ad04
--- /dev/null
+++ b/fledge/tentative/trusted-scoring-signals.https.sub.window.js
@@ -0,0 +1,409 @@
+// META: script=/resources/testdriver.js
+// META: script=/common/utils.js
+// META: script=resources/fledge-util.js
+// META: timeout=long
+
+"use strict";
+
+// These tests focus on trustedScoringSignals: Requesting them, handling network
+// errors, handling the renderURLs portion of the response, passing renderURLs
+// to worklet scripts, and handling the Data-Version header.
+
+// Helper for trusted scoring signals tests. Runs an auction with
+// TRUSTED_SCORING_SIGNALS_URL and a single interest group, failing the test
+// if there's no winner. "scoreAdCheck" is an expression that should be true
+// when evaluated in scoreAd(). "renderURL" can be used to control the response
+// given for TRUSTED_SCORING_SIGNALS_URL.
+async function runTrustedScoringSignalsTest(test, uuid, renderURL, scoreAdCheck) {
+  const auctionConfigOverrides = {
+      trustedScoringSignalsUrl: TRUSTED_SCORING_SIGNALS_URL,
+      decisionLogicUrl:
+          createDecisionScriptUrl(uuid, {
+              scoreAd: `if (!(${scoreAdCheck})) throw "error";` })};
+  await runBasicFledgeTestExpectingWinner(
+      test,
+      { uuid: uuid,
+        interestGroupOverrides: {ads: [{renderUrl: renderURL}]},
+        auctionConfigOverrides: auctionConfigOverrides
+      });
+}
+
+// Much like runTrustedScoringSignalsTest, but runs auctions through reporting
+// as well, and evaluates `check` both in scodeAd() and reportResult(). Also
+// makes sure browserSignals.dataVersion is undefined in generateBid() and
+// reportWin().
+async function runTrustedScoringSignalsDataVersionTest(
+    test, uuid, renderURL, check) {
+  const interestGroupOverrides = {
+      biddingLogicUrl :
+          createBiddingScriptUrl({
+              generateBid:
+                  `if (browserSignals.dataVersion !== undefined)
+                      throw "Bad browserSignals.dataVersion"`,
+              reportWin:
+                  `if (browserSignals.dataVersion !== undefined)
+                     sendReportTo('${createSellerReportUrl(uuid, 'error')}');
+                   else
+                     sendReportTo('${createSellerReportUrl(uuid)}');` }),
+      ads: [{renderUrl: renderURL}]};
+  await joinInterestGroup(test, uuid, interestGroupOverrides);
+
+  const auctionConfigOverrides = {
+    decisionLogicUrl: createDecisionScriptUrl(
+        uuid,
+        { scoreAd:
+              `if (!(${check})) return false;`,
+          reportResult:
+              `if (!(${check}))
+                 sendReportTo('${createSellerReportUrl(uuid, 'error')}')
+               sendReportTo('${createSellerReportUrl(uuid)}')`,
+        }),
+        trustedScoringSignalsUrl: TRUSTED_SCORING_SIGNALS_URL
+  }
+  await runBasicFledgeAuctionAndNavigate(test, uuid, auctionConfigOverrides);
+  await waitForObservedRequests(
+      uuid, [createBidderReportUrl(uuid), createSellerReportUrl(uuid)]);
+}
+
+// Creates a render URL that, when sent to the trusted-scoring-signals.py,
+// results in a trusted scoring signals response with the provided response
+// body.
+function createScoringSignalsRenderUrlWithBody(uuid, responseBody) {
+  return createRenderUrl(uuid, /*script=*/null,
+                         /*signalsParam=*/`replace-body:${responseBody}`);
+}
+
+/////////////////////////////////////////////////////////////////////////////
+// Tests where no renderURL value is received for the passed in renderURL.
+/////////////////////////////////////////////////////////////////////////////
+
+promise_test(async test => {
+  const uuid = generateUuid(test);
+  const decsionLogicScriptUrl = createDecisionScriptUrl(
+      uuid,
+      { scoreAd: 'if (trustedScoringSignals !== null) throw "error";'});
+  await runBasicFledgeTestExpectingWinner(
+      test,
+      { uuid: uuid,
+        auctionConfigOverrides: { decisionLogicUrl: decsionLogicScriptUrl }
+      });
+}, 'No trustedScoringSignalsUrl.');
+
+promise_test(async test => {
+  const uuid = generateUuid(test);
+  const renderURL = createRenderUrl(uuid, /*script=*/null, /*signalsParam=*/'close-connection');
+  await runTrustedScoringSignalsTest(
+      test, uuid, renderURL,
+      'trustedScoringSignals === null');
+}, 'Trusted scoring signals closes the connection without sending anything.');
+
+promise_test(async test => {
+  const uuid = generateUuid(test);
+  const renderURL = createRenderUrl(uuid, /*script=*/null, /*signalsParam=*/'http-error');
+  await runTrustedScoringSignalsTest(test, uuid, renderURL, 'trustedScoringSignals === null');
+}, 'Trusted scoring signals response is HTTP 404 error.');
+
+promise_test(async test => {
+  const uuid = generateUuid(test);
+  const renderURL = createRenderUrl(uuid, /*script=*/null, /*signalsParam=*/'no-content-type');
+  await runTrustedScoringSignalsTest(test, uuid, renderURL, 'trustedScoringSignals === null');
+}, 'Trusted scoring signals response has no content-type.');
+
+promise_test(async test => {
+  const uuid = generateUuid(test);
+  const renderURL = createRenderUrl(uuid, /*script=*/null, /*signalsParam=*/'wrong-content-type');
+  await runTrustedScoringSignalsTest(test, uuid, renderURL, 'trustedScoringSignals === null');
+}, 'Trusted scoring signals response has wrong content-type.');
+
+promise_test(async test => {
+  const uuid = generateUuid(test);
+  const renderURL = createRenderUrl(uuid, /*script=*/null, /*signalsParam=*/'ad-auction-not-allowed');
+  await runTrustedScoringSignalsTest(test, uuid, renderURL, 'trustedScoringSignals === null');
+}, 'Trusted scoring signals response does not allow FLEDGE.');
+
+promise_test(async test => {
+  const uuid = generateUuid(test);
+  const renderURL = createRenderUrl(uuid, /*script=*/null, /*signalsParam=*/'bad-ad-auction-allowed');
+  await runTrustedScoringSignalsTest(test, uuid, renderURL, 'trustedScoringSignals === null');
+}, 'Trusted scoring signals response has wrong Ad-Auction-Allowed header.');
+
+promise_test(async test => {
+  const uuid = generateUuid(test);
+  const renderURL = createRenderUrl(uuid, /*script=*/null, /*signalsParam=*/'no-ad-auction-allow');
+  await runTrustedScoringSignalsTest( test, uuid, renderURL, 'trustedScoringSignals === null');
+}, 'Trusted scoring signals response has no Ad-Auction-Allowed header.');
+
+promise_test(async test => {
+  const uuid = generateUuid(test);
+  const renderURL = createScoringSignalsRenderUrlWithBody(
+      uuid, /*responseBody=*/'');
+  await runTrustedScoringSignalsTest(test, uuid, renderURL, 'trustedScoringSignals === null');
+}, 'Trusted scoring signals response has no body.');
+
+promise_test(async test => {
+  const uuid = generateUuid(test);
+  const renderURL = createScoringSignalsRenderUrlWithBody(
+      uuid, /*responseBody=*/'Not JSON');
+  await runTrustedScoringSignalsTest(test, uuid, renderURL, 'trustedScoringSignals === null');
+}, 'Trusted scoring signals response is not JSON.');
+
+promise_test(async test => {
+  const uuid = generateUuid(test);
+  const renderURL = createScoringSignalsRenderUrlWithBody(
+      uuid, /*responseBody=*/'[]');
+  await runTrustedScoringSignalsTest(test, uuid, renderURL, 'trustedScoringSignals === null');
+}, 'Trusted scoring signals response is a JSON array.');
+
+promise_test(async test => {
+  const uuid = generateUuid(test);
+  const renderURL = createScoringSignalsRenderUrlWithBody(
+      uuid, /*responseBody=*/'{JSON_keys_need_quotes: 1}');
+  await runTrustedScoringSignalsTest(test, uuid, renderURL, 'trustedScoringSignals === null');
+}, 'Trusted scoring signals response is invalid JSON object.');
+
+promise_test(async test => {
+  const uuid = generateUuid(test);
+  const renderURL = createScoringSignalsRenderUrlWithBody(
+      uuid, /*responseBody=*/'{}');
+  await runTrustedScoringSignalsTest(
+      test, uuid, renderURL,
+      `trustedScoringSignals.renderURL["${renderURL}"] === null`);
+}, 'Trusted scoring signals response has no renderUrl object.');
+
+promise_test(async test => {
+  const uuid = generateUuid(test);
+  const renderURL = createScoringSignalsRenderUrlWithBody(
+      uuid, /*responseBody=*/'{"renderUrls":{}}');
+  await runTrustedScoringSignalsTest(
+      test, uuid, renderURL,
+      `trustedScoringSignals.renderURL["${renderURL}"] === null`);
+}, 'Trusted scoring signals response has no renderUrls.');
+
+promise_test(async test => {
+  const uuid = generateUuid(test);
+  const renderURL = createScoringSignalsRenderUrlWithBody(
+      uuid, /*responseBody=*/'{"renderUrls":{"https://wrong-url.test": 5}}');
+  await runTrustedScoringSignalsTest(
+      test, uuid, renderURL,
+      `trustedScoringSignals.renderURL["${renderURL}"] === null &&
+       trustedScoringSignals["https://wrong-url.test/"] === undefined`);
+}, 'Trusted scoring signals response has renderURL not in response.');
+
+/////////////////////////////////////////////////////////////////////////////
+// Tests where renderURL value is received for the passed in renderURL.
+/////////////////////////////////////////////////////////////////////////////
+
+promise_test(async test => {
+  const uuid = generateUuid(test);
+  const renderURL = createRenderUrl(uuid, /*script=*/null, /*signalsParam=*/'null-value');
+  await runTrustedScoringSignalsTest(
+      test, uuid, renderURL,
+      `trustedScoringSignals.renderURL["${renderURL}"] === null`);
+}, 'Trusted scoring signals response has null value for renderURL.');
+
+promise_test(async test => {
+  const uuid = generateUuid(test);
+  const renderURL = createRenderUrl(uuid, /*script=*/null, /*signalsParam=*/'num-value');
+  await runTrustedScoringSignalsTest(
+      test, uuid, renderURL,
+      `trustedScoringSignals.renderURL["${renderURL}"] === 1`);
+}, 'Trusted scoring signals response has a number value for renderURL.');
+
+promise_test(async test => {
+  const uuid = generateUuid(test);
+  const renderURL = createRenderUrl(uuid, /*script=*/null,
+      /*signalsParam=*/'string-value');
+  await runTrustedScoringSignalsTest(
+      test, uuid, renderURL,
+      `trustedScoringSignals.renderURL["${renderURL}"] === "1"`);
+}, 'Trusted scoring signals response has a string value for renderURL.');
+
+promise_test(async test => {
+  const uuid = generateUuid(test);
+  const renderURL = createRenderUrl(uuid, /*script=*/null, /*signalsParam=*/'array-value');
+  await runTrustedScoringSignalsTest(
+      test, uuid, renderURL,
+      `JSON.stringify(trustedScoringSignals.renderURL["${renderURL}"]) === '[1,"foo",null]'`);
+}, 'Trusted scoring signals response has an array value for renderURL.');
+
+promise_test(async test => {
+  const uuid = generateUuid(test);
+  const renderURL = createRenderUrl(uuid, /*script=*/null, /*signalsParam=*/'object-value');
+  await runTrustedScoringSignalsTest(
+      test, uuid, renderURL,
+      `Object.keys(trustedScoringSignals.renderURL["${renderURL}"]).length  === 2 &&
+      trustedScoringSignals.renderURL["${renderURL}"]["a"] === "b" &&
+       JSON.stringify(trustedScoringSignals.renderURL["${renderURL}"]["c"]) === '["d"]'`);
+}, 'Trusted scoring signals response has an object value for renderURL.');
+
+promise_test(async test => {
+  const uuid = generateUuid(test);
+  const renderURL = createRenderUrl(uuid, /*script=*/null, /*signalsParam=*/'+%20 \x00?,3#&');
+  await runTrustedScoringSignalsTest(
+      test, uuid, renderURL,
+      `trustedScoringSignals.renderURL["${renderURL}"] === "default value"`);
+}, 'Trusted scoring signals with escaped renderURL.');
+
+promise_test(async test => {
+  const uuid = generateUuid(test);
+  const renderURL = createRenderUrl(uuid, /*script=*/null, /*signalsParam=*/'hostname');
+  await runTrustedScoringSignalsTest(
+      test, uuid, renderURL,
+      `trustedScoringSignals.renderURL["${renderURL}"] === "${window.location.hostname}"`);
+}, 'Trusted scoring signals receives hostname field.');
+
+// Joins two interest groups and makes sure the scoring signals for one are never leaked
+// to the seller script when scoring the other.
+//
+// There's no guarantee in this test that a single request to the server will be made with
+// render URLs from two different IGs, though that's the case this is trying to test -
+// browsers are not required to support batching, and even if they do, joining any two
+// particular requests may be racy.
+promise_test(async test => {
+  const uuid = generateUuid(test);
+  const renderURL1 = createRenderUrl(uuid, /*script=*/null, /*signalsParam=*/'num-value');
+  const renderURL2 = createRenderUrl(uuid, /*script=*/null, /*signalsParam=*/'string-value');
+  await joinInterestGroup(test, uuid, {ads: [{renderUrl: renderURL1}], name: '1'});
+  await joinInterestGroup(test, uuid, {ads: [{renderUrl: renderURL2}], name: '2'});
+  let auctionConfigOverrides = { trustedScoringSignalsUrl: TRUSTED_SCORING_SIGNALS_URL };
+
+  // scoreAd() only accepts the first IG's bid, validating its trustedScoringSignals.
+  auctionConfigOverrides.decisionLogicUrl =
+        createDecisionScriptUrl(uuid, {
+            scoreAd: `if (browserSignals.renderURL === "${renderURL1}" &&
+                          trustedScoringSignals.renderURL["${renderURL1}"] !== 1 ||
+                          trustedScoringSignals.renderURL["${renderURL2}"] !== undefined)
+                        return;` });
+  let config = await runBasicFledgeAuction(
+      test, uuid, auctionConfigOverrides);
+  assert_true(config instanceof FencedFrameConfig,
+      `Wrong value type returned from first auction: ${config.constructor.type}`);
+
+  // scoreAd() only accepts the second IG's bid, validating its trustedScoringSignals.
+  auctionConfigOverrides.decisionLogicUrl =
+        createDecisionScriptUrl(uuid, {
+            scoreAd: `if (browserSignals.renderURL === "${renderURL2}" &&
+                          trustedScoringSignals.renderURL["${renderURL1}"] !== undefined ||
+                          trustedScoringSignals.renderURL["${renderURL2}"] !== '1')
+                        return;` });
+  config = await runBasicFledgeAuction(
+      test, uuid, auctionConfigOverrides);
+  assert_true(config instanceof FencedFrameConfig,
+      `Wrong value type returned from second auction: ${config.constructor.type}`);
+}, 'Trusted scoring signals multiple renderURLs.');
+
+/////////////////////////////////////////////////////////////////////////////
+// Data-Version tests
+/////////////////////////////////////////////////////////////////////////////
+
+promise_test(async test => {
+  const uuid = generateUuid(test);
+  const renderURL = createRenderUrl(uuid);
+  await runTrustedScoringSignalsTest(
+      test, uuid, renderURL,
+      'browserSignals.dataVersion === undefined');
+}, 'Trusted scoring signals response has no data-version.');
+
+promise_test(async test => {
+  const uuid = generateUuid(test);
+  const renderURL = createRenderUrl(uuid, /*script=*/null, 'data-version:3');
+  await runTrustedScoringSignalsTest(
+      test, uuid, renderURL,
+      'browserSignals.dataVersion === 3');
+}, 'Trusted scoring signals response has valid data-version.');
+
+promise_test(async test => {
+  const uuid = generateUuid(test);
+  const renderURL = createRenderUrl(uuid, /*script=*/null, 'data-version:0');
+  await runTrustedScoringSignalsTest(
+      test, uuid, renderURL,
+      'browserSignals.dataVersion === 0');
+}, 'Trusted scoring signals response has min data-version.');
+
+promise_test(async test => {
+  const uuid = generateUuid(test);
+  const renderURL = createRenderUrl(uuid, /*script=*/null, 'data-version:4294967295');
+  await runTrustedScoringSignalsTest(
+      test, uuid, renderURL,
+      'browserSignals.dataVersion === 4294967295');
+}, 'Trusted scoring signals response has max data-version.');
+
+promise_test(async test => {
+  const uuid = generateUuid(test);
+  const renderURL = createRenderUrl(uuid, /*script=*/null, 'data-version:4294967296');
+  await runTrustedScoringSignalsTest(
+      test, uuid, renderURL,
+      'browserSignals.dataVersion === undefined');
+}, 'Trusted scoring signals response has too large data-version.');
+
+promise_test(async test => {
+  const uuid = generateUuid(test);
+  const renderURL = createRenderUrl(uuid, /*script=*/null, 'data-version:03');
+  await runTrustedScoringSignalsTest(
+      test, uuid, renderURL,
+      'browserSignals.dataVersion === undefined');
+}, 'Trusted scoring signals response has data-version with leading 0.');
+
+promise_test(async test => {
+  const uuid = generateUuid(test);
+  const renderURL = createRenderUrl(uuid, /*script=*/null, 'data-version:-1');
+  await runTrustedScoringSignalsTest(
+      test, uuid, renderURL,
+      'browserSignals.dataVersion === undefined');
+}, 'Trusted scoring signals response has negative data-version.');
+
+promise_test(async test => {
+  const uuid = generateUuid(test);
+  const renderURL = createRenderUrl(uuid, /*script=*/null, 'data-version:1.3');
+  await runTrustedScoringSignalsTest(
+      test, uuid, renderURL,
+      'browserSignals.dataVersion === undefined');
+}, 'Trusted scoring signals response has decimal in data-version.');
+
+promise_test(async test => {
+  const uuid = generateUuid(test);
+  const renderURL = createRenderUrl(uuid, /*script=*/null, 'data-version:2 2');
+  await runTrustedScoringSignalsTest(
+      test, uuid, renderURL,
+      'browserSignals.dataVersion === undefined');
+}, 'Trusted scoring signals response has space in data-version.');
+
+promise_test(async test => {
+  const uuid = generateUuid(test);
+  const renderURL = createRenderUrl(uuid, /*script=*/null, 'data-version:0x4');
+  await runTrustedScoringSignalsTest(
+      test, uuid, renderURL,
+      'browserSignals.dataVersion === undefined');
+}, 'Trusted scoring signals response has space hax data-version.');
+
+promise_test(async test => {
+  const uuid = generateUuid(test);
+  const renderURL = createRenderUrl(uuid, /*script=*/null, 'data-version:3,replace-body:');
+  await runTrustedScoringSignalsTest(
+      test, uuid, renderURL,
+      'browserSignals.dataVersion === undefined');
+}, 'Trusted scoring signals response has data-version and empty body.');
+
+promise_test(async test => {
+  const uuid = generateUuid(test);
+  const renderURL = createRenderUrl(uuid, /*script=*/null, 'data-version:3,replace-body:[]');
+  await runTrustedScoringSignalsTest(
+      test, uuid, renderURL,
+      'browserSignals.dataVersion === undefined');
+}, 'Trusted scoring signals response has data-version and JSON array body.');
+
+promise_test(async test => {
+  const uuid = generateUuid(test);
+  const renderURL = createRenderUrl(uuid, /*script=*/null, 'data-version:3,replace-body:{} {}');
+  await runTrustedScoringSignalsTest(
+      test, uuid, renderURL,
+      'browserSignals.dataVersion === undefined');
+}, 'Trusted scoring signals response has data-version and double JSON object body.');
+
+promise_test(async test => {
+  const uuid = generateUuid(test);
+  const renderURL = createRenderUrl(uuid, /*script=*/null, 'data-version:3,replace-body:{}');
+  await runTrustedScoringSignalsTest(
+      test, uuid, renderURL,
+      'browserSignals.dataVersion === 3');
+}, 'Trusted scoring signals response has data-version and no renderURLs.');