summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/private-aggregation
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /testing/web-platform/tests/private-aggregation
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/private-aggregation')
-rw-r--r--testing/web-platform/tests/private-aggregation/protected-audience-surface-failure.https.html70
-rw-r--r--testing/web-platform/tests/private-aggregation/protected-audience-surface-success.https.html98
-rw-r--r--testing/web-platform/tests/private-aggregation/resources/protected-audience-helper-module.js209
-rw-r--r--testing/web-platform/tests/private-aggregation/resources/protected_audience_event_level_report_handler.py43
-rw-r--r--testing/web-platform/tests/private-aggregation/resources/reports.py88
-rw-r--r--testing/web-platform/tests/private-aggregation/resources/shared-storage-helper-module.js23
-rw-r--r--testing/web-platform/tests/private-aggregation/resources/util.js24
-rw-r--r--testing/web-platform/tests/private-aggregation/shared-storage-permissions-policy-none.https.html23
-rw-r--r--testing/web-platform/tests/private-aggregation/shared-storage-permissions-policy-none.https.html.headers1
-rw-r--r--testing/web-platform/tests/private-aggregation/shared-storage-surface-context-id.https.html73
-rw-r--r--testing/web-platform/tests/private-aggregation/shared-storage-surface-failure-2.https.html26
-rw-r--r--testing/web-platform/tests/private-aggregation/shared-storage-surface-failure.https.html74
-rw-r--r--testing/web-platform/tests/private-aggregation/shared-storage-surface-success-2.https.html27
-rw-r--r--testing/web-platform/tests/private-aggregation/shared-storage-surface-success.https.html71
14 files changed, 850 insertions, 0 deletions
diff --git a/testing/web-platform/tests/private-aggregation/protected-audience-surface-failure.https.html b/testing/web-platform/tests/private-aggregation/protected-audience-surface-failure.https.html
new file mode 100644
index 0000000000..b512afc25d
--- /dev/null
+++ b/testing/web-platform/tests/private-aggregation/protected-audience-surface-failure.https.html
@@ -0,0 +1,70 @@
+<!doctype html>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/protected-audience-helper-module.js"></script>
+
+<body>
+<script>
+'use strict';
+
+promise_test(async test => {
+ const uuid = generateUuid();
+ const contribution = '{ bucket: 340282366920938463463374607431768211456n, value: 1 }';
+
+ await runReportTest(test, uuid,
+ { scoreAd:
+ `try {
+ privateAggregation.contributeToHistogram(${contribution});
+ } catch {
+ forDebuggingOnly.reportAdAuctionWin('${createWritingURL(uuid)}');
+ }`},
+ /*expectedNumReports=*/ 1);
+}, 'contributeToHistogram() with too large bucket in scoreAd()');
+
+promise_test(async test => {
+ const uuid = generateUuid();
+ const contribution = '{ bucket: -1n, value: 1 }';
+
+ await runReportTest(test, uuid,
+ { reportResult:
+ `try {
+ privateAggregation.contributeToHistogram(${contribution});
+ } catch {
+ sendReportTo('${createWritingURL(uuid)}');
+ }`},
+ /*expectedNumReports=*/ 1);
+}, 'contributeToHistogram() with negative bucket in reportResult()');
+
+
+promise_test(async test => {
+ const uuid = generateUuid();
+ const contribution = '{ bucket: 1, value: 1 }';
+
+ await runReportTest(test, uuid,
+ { generateBid:
+ `try {
+ privateAggregation.contributeToHistogram(${contribution});
+ } catch {
+ forDebuggingOnly.reportAdAuctionWin('${createWritingURL(uuid)}');
+ }`},
+ /*expectedNumReports=*/ 1);
+}, 'contributeToHistogram() with non-BigInt bucket in generateBid()');
+
+promise_test(async test => {
+ const uuid = generateUuid();
+ const contribution = '{ bucket: 1n, value: -1 }';
+
+ await runReportTest(test, uuid,
+ { reportWin:
+ `try {
+ privateAggregation.contributeToHistogram(${contribution});
+ } catch {
+ sendReportTo('${createWritingURL(uuid)}');
+ }`},
+ /*expectedNumReports=*/ 1);
+}, 'contributeToHistogram() with negative value in reportWin()');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/private-aggregation/protected-audience-surface-success.https.html b/testing/web-platform/tests/private-aggregation/protected-audience-surface-success.https.html
new file mode 100644
index 0000000000..1d89032109
--- /dev/null
+++ b/testing/web-platform/tests/private-aggregation/protected-audience-surface-success.https.html
@@ -0,0 +1,98 @@
+<!doctype html>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="resources/protected-audience-helper-module.js"></script>
+
+<body>
+<script>
+'use strict';
+
+promise_test(async test => {
+ const uuid = generateUuid();
+ const contribution = '{ bucket: 1n, value: 1 }';
+
+ await runReportTest(test, uuid,
+ { scoreAd:
+ `privateAggregation.contributeToHistogram(${contribution});
+ forDebuggingOnly.reportAdAuctionWin('${createWritingURL(uuid)}');`
+ },
+ /*expectedNumReports=*/ 1);
+}, 'contributeToHistogram() with positive bucket in scoreAd()');
+
+promise_test(async test => {
+ const uuid = generateUuid();
+ const contribution = '{ bucket: 0n, value: 1 }';
+
+ await runReportTest(test, uuid,
+ { reportResult:
+ `privateAggregation.contributeToHistogram(${contribution});
+ sendReportTo('${createWritingURL(uuid)}');`
+ },
+ /*expectedNumReports=*/ 1);
+}, 'contributeToHistogram() with zero bucket in reportResult()');
+
+
+promise_test(async test => {
+ const uuid = generateUuid();
+ const contribution = '{ bucket: 18446744073709551616n, value: 1 }';
+
+ await runReportTest(test, uuid,
+ { generateBid:
+ `privateAggregation.contributeToHistogram(${contribution});
+ forDebuggingOnly.reportAdAuctionWin('${createWritingURL(uuid)}');`
+ },
+ /*expectedNumReports=*/ 1);
+}, 'contributeToHistogram() with large bucket in generateBid()');
+
+promise_test(async test => {
+ const uuid = generateUuid();
+ const contribution = '{ bucket: 340282366920938463463374607431768211455n, value: 1 }';
+
+ await runReportTest(test, uuid,
+ { reportWin:
+ `privateAggregation.contributeToHistogram(${contribution});
+ sendReportTo('${createWritingURL(uuid)}');`
+ },
+ /*expectedNumReports=*/ 1);
+}, 'contributeToHistogram() with max bucket in reportWin()');
+
+promise_test(async test => {
+ const uuid = generateUuid();
+ const contribution = '{ bucket: 1n, value: 1 }';
+
+ await runReportTest(test, uuid,
+ { scoreAd:
+ `privateAggregation.contributeToHistogram(${contribution});
+ forDebuggingOnly.reportAdAuctionWin('${createWritingURL(uuid)}');`
+ },
+ /*expectedNumReports=*/ 1);
+}, 'contributeToHistogram() with positive value in scoreAd()');
+
+promise_test(async test => {
+ const uuid = generateUuid();
+ const contribution = '{ bucket: 1n, value: 0 }';
+
+ await runReportTest(test, uuid,
+ { reportResult:
+ `privateAggregation.contributeToHistogram(${contribution});
+ sendReportTo('${createWritingURL(uuid)}');`
+ },
+ /*expectedNumReports=*/ 1);
+}, 'contributeToHistogram() with zero value in reportResult()');
+
+promise_test(async test => {
+ const uuid = generateUuid();
+ const contribution = '{ bucket: 1n, value: 2.3 }';
+
+ await runReportTest(test, uuid,
+ { generateBid:
+ `privateAggregation.contributeToHistogram(${contribution});
+ forDebuggingOnly.reportAdAuctionWin('${createWritingURL(uuid)}');`
+ },
+ /*expectedNumReports=*/ 1);
+}, 'contributeToHistogram() with non-integer value in generateBid()');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/private-aggregation/resources/protected-audience-helper-module.js b/testing/web-platform/tests/private-aggregation/resources/protected-audience-helper-module.js
new file mode 100644
index 0000000000..be4f01379e
--- /dev/null
+++ b/testing/web-platform/tests/private-aggregation/resources/protected-audience-helper-module.js
@@ -0,0 +1,209 @@
+// This file is adapted from /fledge/tentative/resources/fledge-util.js,
+// removing unnecessary logic and modifying to allow it to be run in the
+// private-aggregation directory.
+
+"use strict;"
+
+const FULL_URL = window.location.href;
+let 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';
+
+// Use python script files under fledge directory
+const FLEDGE_DIR = '/fledge/tentative/';
+const FLEDGE_BASE_URL = BASE_URL.replace(BASE_PATH, FLEDGE_DIR);
+
+// Sleep method that waits for prescribed number of milliseconds.
+const sleep = ms => new Promise(resolve => step_timeout(resolve, ms));
+
+// Generates a UUID by token.
+function generateUuid() {
+ let uuid = token();
+ return uuid;
+}
+
+// Creates a URL that will be sent to the handler.
+// `uuid` is used to identify the stash shard to use.
+// `operate` is used to set action as write or read.
+// `report` is used to carry the message for write requests.
+function createReportingURL(uuid, operation, report = 'default-report') {
+ let url = new URL(`${window.location.origin}${BASE_PATH}resources/protected_audience_event_level_report_handler.py`);
+ url.searchParams.append('uuid', uuid);
+ url.searchParams.append('operation', operation);
+
+ if (report)
+ url.searchParams.append('report', report);
+
+ return url.toString();
+}
+
+function createWritingURL(uuid, report) {
+ return createReportingURL(uuid, 'write');
+}
+
+function createReadingURL(uuid) {
+ return createReportingURL(uuid, 'read');
+}
+
+async function waitForObservedReports(uuid, expectedNumReports, timeout = 5000 /*ms*/) {
+ expectedReports = Array(expectedNumReports).fill('default-report');
+ const reportURL = createReadingURL(uuid);
+ let startTime = performance.now();
+
+ while (performance.now() - startTime < timeout) {
+ let response = await fetch(reportURL, { credentials: 'omit', mode: 'cors' });
+ let actualReports = await response.json();
+
+ // If expected number of reports have been observed, compare with list of
+ // all expected reports and exit.
+ if (actualReports.length == expectedReports.length) {
+ assert_array_equals(actualReports.sort(), expectedReports);
+ return;
+ }
+
+ await sleep(/*ms=*/ 100);
+ }
+ assert_unreached("Report fetching timed out: " + uuid);
+}
+
+// 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(`${FLEDGE_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(`${FLEDGE_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(`${FLEDGE_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}".
+// `interestGroupOverrides` is required 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,
+ ads: [{renderURL: createRenderURL(uuid)}],
+ ...interestGroupOverrides
+ };
+
+ await navigator.joinAdInterestGroup(interestGroup,
+ INTEREST_GROUP_LIFETIME_SECS);
+ test.add_cleanup(
+ async () => {await navigator.leaveAdInterestGroup(interestGroup)});
+}
+
+// Runs a FLEDGE auction and returns the result. `auctionConfigOverrides` is
+// required to override fields in the auction configuration.
+async function runBasicFledgeAuction(test, uuid, auctionConfigOverrides) {
+ let auctionConfig = {
+ seller: window.location.origin,
+ 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 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 scoreAd(), reportResult(), generateBid() and reportWin(), as well as
+// additional arguments to be passed to joinAdInterestGroup() and runAdAuction()
+async function runReportTest(test, uuid, codeToInsert,
+ expectedNumReports = 0, overrides = {}) {
+ let generateBid = codeToInsert.generateBid;
+ let scoreAd = codeToInsert.scoreAd;
+ let reportWin = codeToInsert.reportWin;
+ let reportResult = codeToInsert.reportResult;
+
+ let extraInterestGroupOverrides = overrides.joinAdInterestGroup || {}
+ let extraAuctionConfigOverrides = overrides.runAdAuction || {}
+
+ let interestGroupOverrides = {
+ biddingLogicURL: createBiddingScriptURL({ generateBid, reportWin }),
+ ...extraInterestGroupOverrides
+ };
+ let auctionConfigOverrides = {
+ decisionLogicURL: createDecisionScriptURL(
+ uuid, { scoreAd, reportResult }),
+ ...extraAuctionConfigOverrides
+ }
+
+ await joinInterestGroup(test, uuid, interestGroupOverrides);
+ await runBasicFledgeAuctionAndNavigate(test, uuid, auctionConfigOverrides);
+
+ if (expectedNumReports) {
+ await waitForObservedReports(uuid, expectedNumReports);
+ }
+}
diff --git a/testing/web-platform/tests/private-aggregation/resources/protected_audience_event_level_report_handler.py b/testing/web-platform/tests/private-aggregation/resources/protected_audience_event_level_report_handler.py
new file mode 100644
index 0000000000..c77b9e19c8
--- /dev/null
+++ b/testing/web-platform/tests/private-aggregation/resources/protected_audience_event_level_report_handler.py
@@ -0,0 +1,43 @@
+"""Handler to receive message from protected audience worklets, such as
+sendReportTo() and forDebuggingOnly.reportAdAuctionWin().
+
+This handler only supports read and write operations from the URL parameters.
+"""
+
+import json
+from typing import List, Tuple
+
+from wptserve.request import Request
+from wptserve.response import Response
+
+Header = Tuple[str, str]
+ResponseTuple = Tuple[int, List[Header], str]
+
+def main(request: Request, response: Response) -> ResponseTuple:
+ operation = request.GET.first(b"operation").decode('utf-8')
+ uuid = request.GET.first(b"uuid").decode('utf-8')
+ if operation == "read":
+ with request.server.stash.lock:
+ stash_reports = request.server.stash.take(key=uuid)
+ if stash_reports is None:
+ stash_reports = []
+ else:
+ request.server.stash.put(key=uuid, value=stash_reports)
+
+ return 200, [("Content-Type", "application/json")], json.dumps(stash_reports)
+ elif operation == "write":
+ report = request.GET.first(b"report").decode('utf-8')
+
+ if report is None:
+ return 400, [("Content-Type", "application/json")], json.dumps({'error': 'Missing report.', 'uuid': uuid})
+
+ with request.server.stash.lock:
+ stash_reports = request.server.stash.take(key=uuid)
+ if stash_reports is None:
+ stash_reports = []
+ stash_reports.append(report)
+ request.server.stash.put(key=uuid, value=stash_reports)
+
+ return 200, [("Content-Type", "application/json")], json.dumps({'msg': 'Recorded report ' + uuid})
+ else:
+ return 400, [("Content-Type", "application/json")], json.dumps({'error': 'Invalid operation.', 'uuid': uuid})
diff --git a/testing/web-platform/tests/private-aggregation/resources/reports.py b/testing/web-platform/tests/private-aggregation/resources/reports.py
new file mode 100644
index 0000000000..0f6fd8255a
--- /dev/null
+++ b/testing/web-platform/tests/private-aggregation/resources/reports.py
@@ -0,0 +1,88 @@
+"""Methods for the report-shared-storage and report-protected-audience endpoints (including debug endpoints)"""
+import json
+from typing import List, Optional, Tuple, Union
+import urllib.parse
+
+from wptserve.request import Request
+from wptserve.stash import Stash
+from wptserve.utils import isomorphic_decode, isomorphic_encode
+
+# Arbitrary key used to access the reports in the stash.
+REPORTS_KEY = "9d285691-4386-45ad-9a79-d2ec29557bfe"
+
+CLEAR_STASH_AS_BYTES = isomorphic_encode("clear_stash")
+
+Header = Tuple[str, str]
+Status = Union[int, Tuple[int, str]]
+Response = Tuple[Status, List[Header], str]
+
+def get_request_origin(request: Request) -> str:
+ return "%s://%s" % (request.url_parts.scheme,
+ request.url_parts.netloc)
+
+def handle_post_request(request: Request) -> Response:
+ """Handles POST request for reports.
+
+ Retrieves the report from the request body and stores the report in the
+ stash. If clear_stash is specified in the query params, clears the stash.
+ """
+ if request.GET.get(CLEAR_STASH_AS_BYTES):
+ clear_stash(request.server.stash)
+ return 200, [], "Stash successfully cleared."
+
+ store_report(request.server.stash, get_request_origin(request),
+ request.body.decode("utf-8"))
+ return 200, [], ""
+
+
+def handle_get_request(request: Request) -> Response:
+ """Handles GET request for reports.
+
+ Retrieves and returns all reports from the stash.
+ """
+ headers = [("Content-Type", "application/json")]
+ reports = take_reports(request.server.stash, get_request_origin(request))
+ headers.append(("Access-Control-Allow-Origin", "*"))
+ return 200, headers, json.dumps(reports)
+
+
+def store_report(stash: Stash, origin: str, report: str) -> None:
+ """Stores the report in the stash. Report here is a JSON."""
+ with stash.lock:
+ reports_dict = stash.take(REPORTS_KEY)
+ if not reports_dict:
+ reports_dict = {}
+ reports = reports_dict.get(origin, [])
+ reports.append(report)
+ reports_dict[origin] = reports
+ stash.put(REPORTS_KEY, reports_dict)
+ return None
+
+def clear_stash(stash: Stash) -> None:
+ "Clears the stash."
+ stash.take(REPORTS_KEY)
+ return None
+
+def take_reports(stash: Stash, origin: str) -> List[str]:
+ """Takes all the reports from the stash and returns them."""
+ with stash.lock:
+ reports_dict = stash.take(REPORTS_KEY)
+ if not reports_dict:
+ reports_dict = {}
+
+ reports = reports_dict.pop(origin, [])
+ stash.put(REPORTS_KEY, reports_dict)
+ return reports
+
+
+def handle_request(request: Request) -> Response:
+ """Handles request to get or store reports."""
+ if request.method == "POST":
+ return handle_post_request(request)
+ if request.method == "GET":
+ return handle_get_request(request)
+
+ return (405, "Method Not Allowed"), [("Content-Type", "application/json")], json.dumps({
+ "code": 405,
+ "message": "Only GET or POST methods are supported."
+ })
diff --git a/testing/web-platform/tests/private-aggregation/resources/shared-storage-helper-module.js b/testing/web-platform/tests/private-aggregation/resources/shared-storage-helper-module.js
new file mode 100644
index 0000000000..f5a8533d0f
--- /dev/null
+++ b/testing/web-platform/tests/private-aggregation/resources/shared-storage-helper-module.js
@@ -0,0 +1,23 @@
+// Copyright 2023 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+class ContributeToHistogramOperation {
+ async run(urls, data) {
+ if (data.enableDebugMode) {
+ privateAggregation.enableDebugMode(data.enableDebugModeArgs);
+
+ if (data.enableDebugModeExtraTime) {
+ privateAggregation.enableDebugMode(data.enableDebugModeArgs);
+ }
+ }
+ for (const contribution of data.contributions) {
+ privateAggregation.contributeToHistogram(contribution);
+ }
+
+ // If an error occurs, the default URL will be picked instead.
+ return 1;
+ }
+}
+
+register('contribute-to-histogram', ContributeToHistogramOperation);
diff --git a/testing/web-platform/tests/private-aggregation/resources/util.js b/testing/web-platform/tests/private-aggregation/resources/util.js
new file mode 100644
index 0000000000..24e156446f
--- /dev/null
+++ b/testing/web-platform/tests/private-aggregation/resources/util.js
@@ -0,0 +1,24 @@
+// Execute Private Aggregation functions in shared storage worklet given
+// `paa_data`, and expect that success/failure result is `expected_error`.
+async function VerifyContributeToHistogram(paa_data, expected_error) {
+ const ancestor_key = token();
+ let url0 = generateURL("/shared-storage/resources/frame0.html",
+ [ancestor_key]);
+ let url1 = generateURL("/shared-storage/resources/frame1.html",
+ [ancestor_key]);
+
+ await addModuleOnce("/private-aggregation/resources/shared-storage-helper-module.js");
+
+ let select_url_result = await sharedStorage.selectURL(
+ "contribute-to-histogram", [{url: url0}, {url: url1}],
+ {data: paa_data, keepAlive: true});
+
+ attachFencedFrame(select_url_result, 'opaque-ads');
+ const result = await nextValueFromServer(ancestor_key);
+
+ if (expected_error) {
+ assert_equals(result, "frame0_loaded");
+ } else {
+ assert_equals(result, "frame1_loaded");
+ }
+}
diff --git a/testing/web-platform/tests/private-aggregation/shared-storage-permissions-policy-none.https.html b/testing/web-platform/tests/private-aggregation/shared-storage-permissions-policy-none.https.html
new file mode 100644
index 0000000000..3593ed71ea
--- /dev/null
+++ b/testing/web-platform/tests/private-aggregation/shared-storage-permissions-policy-none.https.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/shared-storage/resources/util.js"></script>
+<script src="/private-aggregation/resources/util.js"></script>
+<script src="/fenced-frame/resources/utils.js"></script>
+
+<body>
+<script>
+'use strict';
+
+promise_test(async () => {
+ const paa_data = {
+ enableDebugMode: true,
+ contributions: [{bucket: 1n, value: 2}]
+ };
+
+ await VerifyContributeToHistogram(paa_data, /*expected_error=*/true);
+}, 'contributeToHistogram() with disabled "private-aggregation" permissions policy');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/private-aggregation/shared-storage-permissions-policy-none.https.html.headers b/testing/web-platform/tests/private-aggregation/shared-storage-permissions-policy-none.https.html.headers
new file mode 100644
index 0000000000..8781910151
--- /dev/null
+++ b/testing/web-platform/tests/private-aggregation/shared-storage-permissions-policy-none.https.html.headers
@@ -0,0 +1 @@
+Permissions-Policy: private-aggregation=()
diff --git a/testing/web-platform/tests/private-aggregation/shared-storage-surface-context-id.https.html b/testing/web-platform/tests/private-aggregation/shared-storage-surface-context-id.https.html
new file mode 100644
index 0000000000..3b0e1b3d74
--- /dev/null
+++ b/testing/web-platform/tests/private-aggregation/shared-storage-surface-context-id.https.html
@@ -0,0 +1,73 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/shared-storage/resources/util.js"></script>
+<script src="/private-aggregation/resources/util.js"></script>
+<script src="/fenced-frame/resources/utils.js"></script>
+
+<body>
+<script>
+'use strict';
+
+promise_test(async () => {
+ await addModuleOnce("/private-aggregation/resources/private-aggregation-helper-module.js");
+
+ const data = {
+ contributions: [{bucket: 1n, value: 2}]
+ };
+ const privateAggregationConfig = {
+ contextId: "example_context_id"
+ };
+
+ await sharedStorage.run("contribute-to-histogram",
+ {data, privateAggregationConfig, keepAlive: true});
+}, 'set context ID');
+
+promise_test(async () => {
+ await addModuleOnce("/private-aggregation/resources/private-aggregation-helper-module.js");
+
+ const data = {
+ contributions: [{bucket: 1n, value: 2}]
+ };
+ const privateAggregationConfig = {
+ contextId: ""
+ };
+
+ await sharedStorage.run("contribute-to-histogram",
+ {data, privateAggregationConfig, keepAlive: true});
+}, 'set empty context ID');
+
+promise_test(async () => {
+ await addModuleOnce("/private-aggregation/resources/private-aggregation-helper-module.js");
+
+ const data = {
+ contributions: [{bucket: 1n, value: 2}]
+ };
+ const privateAggregationConfig = {
+ contextId: "an_example_of_a_context_id_with_the_exact_maximum_allowed_length"
+ };
+
+ await sharedStorage.run("contribute-to-histogram",
+ {data, privateAggregationConfig, keepAlive: true});
+}, 'set max length context ID');
+
+promise_test(async (test) => {
+ await addModuleOnce("/private-aggregation/resources/private-aggregation-helper-module.js");
+
+ const data = {
+ contributions: [{bucket: 1n, value: 2}]
+ };
+ const privateAggregationConfig = {
+ contextId: "this_is_an_example_of_a_context_id_that_is_too_long_to_be_allowed"
+ };
+
+ return promise_rejects_dom(
+ test, "DataError",
+ sharedStorage.run("contribute-to-histogram",
+ {data, privateAggregationConfig, keepAlive: true}));
+}, 'set too long context ID');
+
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/private-aggregation/shared-storage-surface-failure-2.https.html b/testing/web-platform/tests/private-aggregation/shared-storage-surface-failure-2.https.html
new file mode 100644
index 0000000000..b39ecd8d74
--- /dev/null
+++ b/testing/web-platform/tests/private-aggregation/shared-storage-surface-failure-2.https.html
@@ -0,0 +1,26 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/shared-storage/resources/util.js"></script>
+<script src="/private-aggregation/resources/util.js"></script>
+<script src="/fenced-frame/resources/utils.js"></script>
+
+<body>
+<script>
+'use strict';
+
+// Note: This file should contain at most 6 shared storage subtests due to the
+// budget on selectURL().
+
+promise_test(async () => {
+ const paa_data = {
+ enableDebugMode: true,
+ contributions: [{bucket: 1n, value: -2}]
+ };
+
+ await VerifyContributeToHistogram(paa_data, /*expected_error=*/true);
+}, 'contributeToHistogram() with a negative value');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/private-aggregation/shared-storage-surface-failure.https.html b/testing/web-platform/tests/private-aggregation/shared-storage-surface-failure.https.html
new file mode 100644
index 0000000000..9dc62b1bb7
--- /dev/null
+++ b/testing/web-platform/tests/private-aggregation/shared-storage-surface-failure.https.html
@@ -0,0 +1,74 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/shared-storage/resources/util.js"></script>
+<script src="/private-aggregation/resources/util.js"></script>
+<script src="/fenced-frame/resources/utils.js"></script>
+
+<body>
+<script>
+'use strict';
+
+// Note: This file should contain at most 6 shared storage subtests due to the
+// budget on selectURL().
+
+promise_test(async () => {
+ const paa_data = {
+ enableDebugMode: true,
+ contributions: [{bucket: 340282366920938463463374607431768211456n, value: 2}]
+ };
+
+ await VerifyContributeToHistogram(paa_data, /*expected_error=*/true);
+}, 'contributeToHistogram() with a bucket too large');
+
+promise_test(async () => {
+ const paa_data = {
+ enableDebugMode: true,
+ contributions: [{bucket: -1n, value: 2}]
+ };
+
+ await VerifyContributeToHistogram(paa_data, /*expected_error=*/true);
+}, 'contributeToHistogram() with negative bucket');
+
+promise_test(async () => {
+ const paa_data = {
+ enableDebugMode: true,
+ contributions: [{bucket: 1, value: 2}]
+ };
+
+ await VerifyContributeToHistogram(paa_data, /*expected_error=*/true);
+}, 'contributeToHistogram() with non-BigInt bucket');
+
+promise_test(async () => {
+ const paa_data = {
+ enableDebugMode: true,
+ enableDebugModeArgs: 1234n,
+ contributions: [{bucket: 1n, value: 2}]
+ };
+
+ await VerifyContributeToHistogram(paa_data, /*expected_error=*/true);
+}, 'Invalid enableDebugMode argument');
+
+promise_test(async () => {
+ const paa_data = {
+ enableDebugMode: true,
+ enableDebugModeArgs: {debugKey: 1234},
+ contributions: [{bucket: 1n, value: 2}]
+ };
+
+ await VerifyContributeToHistogram(paa_data, /*expected_error=*/true);
+}, 'contributeToHistogram() with a non-BigInt debugKey');
+
+promise_test(async () => {
+ const paa_data = {
+ enableDebugMode: true,
+ enableDebugModeExtraTime: true,
+ contributions: [{bucket: 1n, value: 2}]
+ };
+
+ await VerifyContributeToHistogram(paa_data, /*expected_error=*/true);
+}, 'enableDebugMode called twice');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/private-aggregation/shared-storage-surface-success-2.https.html b/testing/web-platform/tests/private-aggregation/shared-storage-surface-success-2.https.html
new file mode 100644
index 0000000000..8203fd11ea
--- /dev/null
+++ b/testing/web-platform/tests/private-aggregation/shared-storage-surface-success-2.https.html
@@ -0,0 +1,27 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/shared-storage/resources/util.js"></script>
+<script src="/private-aggregation/resources/util.js"></script>
+<script src="/fenced-frame/resources/utils.js"></script>
+
+<body>
+<script>
+'use strict';
+
+// Note: This file should contain at most 6 shared storage subtests due to the
+// budget on selectURL().
+
+promise_test(async () => {
+ const paa_data = {
+ enableDebugMode: true,
+ enableDebugModeArgs: {debugKey: 1234n},
+ contributions: [{bucket: 1n, value: 2}]
+ };
+
+ await VerifyContributeToHistogram(paa_data, /*expected_error=*/false);
+}, 'contributeToHistogram() with a valid debugKey');
+
+</script>
+</body>
diff --git a/testing/web-platform/tests/private-aggregation/shared-storage-surface-success.https.html b/testing/web-platform/tests/private-aggregation/shared-storage-surface-success.https.html
new file mode 100644
index 0000000000..7012180c71
--- /dev/null
+++ b/testing/web-platform/tests/private-aggregation/shared-storage-surface-success.https.html
@@ -0,0 +1,71 @@
+<!doctype html>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/shared-storage/resources/util.js"></script>
+<script src="/private-aggregation/resources/util.js"></script>
+<script src="/fenced-frame/resources/utils.js"></script>
+
+<body>
+<script>
+'use strict';
+
+// Note: This file should contain at most 6 shared storage subtests due to the
+// budget on selectURL().
+
+promise_test(async () => {
+ const paa_data = {
+ enableDebugMode: true,
+ contributions: [{bucket: 1n, value: 2}]
+ };
+
+ await VerifyContributeToHistogram(paa_data, /*expected_error=*/false);
+}, 'basic contributeToHistogram() test');
+
+promise_test(async () => {
+ const paa_data = {
+ enableDebugMode: true,
+ contributions: [{bucket: 0n, value: 2}]
+ };
+
+ await VerifyContributeToHistogram(paa_data, /*expected_error=*/false);
+}, 'contributeToHistogram() with a zero bucket');
+
+promise_test(async () => {
+ const paa_data = {
+ enableDebugMode: true,
+ contributions: [{bucket: 1n, value: 0}]
+ };
+
+ await VerifyContributeToHistogram(paa_data, /*expected_error=*/false);
+}, 'contributeToHistogram() with a zero value');
+
+promise_test(async () => {
+ const paa_data = {
+ enableDebugMode: true,
+ contributions: [{bucket: 18446744073709551616n, value: 5}]
+ };
+
+ await VerifyContributeToHistogram(paa_data, /*expected_error=*/false);
+}, 'contributeToHistogram() with a large bucket');
+
+promise_test(async () => {
+ const paa_data = {
+ enableDebugMode: true,
+ contributions: [{bucket: 340282366920938463463374607431768211455n, value: 5}]
+ };
+
+ await VerifyContributeToHistogram(paa_data, /*expected_error=*/false);
+}, 'contributeToHistogram() with a max bucket');
+
+promise_test(async () => {
+ const paa_data = {
+ enableDebugMode: true,
+ contributions: [{bucket: 1n, value: 2.3}]
+ };
+
+ await VerifyContributeToHistogram(paa_data, /*expected_error=*/false);
+}, 'contributeToHistogram() with a non-integer value');
+
+</script>
+</body>