-
Notifications
You must be signed in to change notification settings - Fork 6.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add basic end-to-end Private Aggregation Web Platform Tests
These tests ensure that a report makes it to the expected endpoint. Only tests in Shared Storage worklets for now Bug: 1452248 Change-Id: I70ad2180c7cde93beb5ffe08fe3c1a091bf298d9 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4598993 Reviewed-by: Yao Xiao <yaoxia@chromium.org> Auto-Submit: Alex Turner <alexmt@chromium.org> Commit-Queue: Alex Turner <alexmt@chromium.org> Reviewed-by: Mason Freed <masonf@chromium.org> Cr-Commit-Position: refs/heads/main@{#1158824}
- Loading branch information
1 parent
da4efac
commit 540a46b
Showing
8 changed files
with
304 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
7 changes: 7 additions & 0 deletions
7
.../blink/web_tests/external/wpt/.well-known/private-aggregation/debug/report-shared-storage
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
"""Endpoint to receive and return aggregatable reports.""" | ||
from importlib import import_module | ||
|
||
reports = import_module('private-aggregation.resources.reports') | ||
|
||
def main(request, response): | ||
return reports.handle_request(request) |
7 changes: 7 additions & 0 deletions
7
..._party/blink/web_tests/external/wpt/.well-known/private-aggregation/report-shared-storage
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
"""Endpoint to receive and return aggregatable reports.""" | ||
from importlib import import_module | ||
|
||
reports = import_module('private-aggregation.resources.reports') | ||
|
||
def main(request, response): | ||
return reports.handle_request(request) |
77 changes: 77 additions & 0 deletions
77
third_party/blink/web_tests/external/wpt/private-aggregation/resources/reports.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
"""Methods for the report-shared-storage and report-protected-audience endpoints (including debug endpoints)""" | ||
import json | ||
from typing import List, Optional, Tuple, TypedDict | ||
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" | ||
|
||
Header = Tuple[str, str] | ||
Status = 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. | ||
""" | ||
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 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." | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
12 changes: 12 additions & 0 deletions
12
...party/blink/web_tests/wpt_internal/private-aggregation/resources/shared-storage-module.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
class ContributeToHistogramOperation { | ||
async run(data) { | ||
if (data.enableDebugMode) { | ||
privateAggregation.enableDebugMode(); | ||
} | ||
for (const contribution of data.contributions) { | ||
privateAggregation.contributeToHistogram(contribution); | ||
} | ||
} | ||
} | ||
|
||
register('contribute-to-histogram', ContributeToHistogramOperation); |
124 changes: 124 additions & 0 deletions
124
third_party/blink/web_tests/wpt_internal/private-aggregation/resources/utils.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
/** | ||
* Delay method that waits for prescribed number of milliseconds. | ||
*/ | ||
const delay = ms => new Promise(resolve => step_timeout(resolve, ms)); | ||
|
||
/** | ||
* Polls the given `url` to retrieve reports sent there. Once the reports are | ||
* received, returns the list of reports. Returns null if the timeout is reached | ||
* before a report is available. | ||
*/ | ||
const pollReports = async (url, origin = location.origin, timeout = 60 * 1000 /*ms*/) => { | ||
let startTime = performance.now(); | ||
while (performance.now() - startTime < timeout) { | ||
const resp = await fetch(new URL(url, origin)); | ||
const payload = await resp.json(); | ||
if (payload.length > 0) { | ||
return payload; | ||
} | ||
await delay(/*ms=*/ 100); | ||
} | ||
return null; | ||
}; | ||
|
||
/** | ||
* Verifies that a report's shared_info string is serialized JSON with the | ||
* expected fields. `is_debug_enabled` should be a boolean corresponding to | ||
* whether debug mode is expected to be enabled for this report. | ||
*/ | ||
const verifySharedInfo = (shared_info_str, is_debug_enabled) => { | ||
shared_info = JSON.parse(shared_info_str); | ||
assert_equals(shared_info.api, 'shared-storage'); | ||
if (is_debug_enabled) { | ||
assert_equals(shared_info.debug_mode, 'enabled'); | ||
} else { | ||
assert_not_own_property(shared_info.debug_mode); | ||
} | ||
|
||
const uuid_regex = RegExp( | ||
'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'); | ||
assert_own_property(shared_info, 'report_id'); | ||
assert_true(uuid_regex.test(shared_info.report_id)); | ||
|
||
assert_equals(shared_info.reporting_origin, location.origin); | ||
|
||
// The amount of delay is implementation-defined. | ||
const integer_regex = RegExp('^[0-9]*$'); | ||
assert_own_property(shared_info, 'scheduled_report_time'); | ||
assert_true(integer_regex.test(shared_info.scheduled_report_time)); | ||
|
||
assert_equals(shared_info.version, '0.1'); | ||
|
||
// Check there are no extra keys | ||
assert_equals(Object.keys(shared_info).length, is_debug_enabled ? 6 : 5); | ||
}; | ||
|
||
/** | ||
* Verifies that an report's aggregation_service_payloads has the expected | ||
* fields. The `expected_cleartext_payload` should be the expected value of | ||
* debug_cleartext_payload or undefined if debug mode is disabled. | ||
*/ | ||
const verifyAggregationServicePayloads = (aggregation_service_payloads, expected_cleartext_payload) => { | ||
assert_equals(aggregation_service_payloads.length, 1); | ||
const payload_obj = aggregation_service_payloads[0]; | ||
|
||
assert_own_property(payload_obj, 'key_id'); | ||
// The only id specified in the test key file. | ||
assert_equals(payload_obj.key_id, 'example_id'); | ||
|
||
assert_own_property(payload_obj, 'payload'); | ||
// Check the payload is base64 encoded. We do not decrypt the payload to test | ||
// its contents. | ||
atob(payload_obj.payload); | ||
|
||
if (expected_cleartext_payload) { | ||
assert_own_property(payload_obj, 'debug_cleartext_payload'); | ||
assert_equals(payload_obj.debug_cleartext_payload, expected_cleartext_payload); | ||
} | ||
|
||
// Check there are no extra keys | ||
assert_equals(Object.keys(payload_obj).length, expected_cleartext_payload ? 3 : 2); | ||
}; | ||
|
||
/** | ||
* Verifies that an report has the expected fields. `is_debug_enabled` should be | ||
* a boolean corresponding to whether debug mode is expected to be enabled for | ||
* this report. `debug_key` should be the debug key if set; otherwise, | ||
* undefined. The `expected_cleartext_payload` should be the expected value of | ||
* debug_cleartext_payload if debug mode is enabled; otherwise, undefined. | ||
*/ | ||
const verifyReport = (report, is_debug_enabled, debug_key, expected_cleartext_payload) => { | ||
if (debug_key || expected_cleartext_payload) { | ||
// A debug key cannot be set without debug mode being enabled and the | ||
// `expected_cleartext_payload` should be undefined if debug mode is not | ||
// enabled. | ||
assert_true(is_debug_enabled); | ||
} | ||
|
||
assert_own_property(report, 'shared_info'); | ||
verifySharedInfo(report.shared_info, is_debug_enabled); | ||
|
||
if (debug_key) { | ||
assert_equals(report.debug_key, debug_key); | ||
} else { | ||
assert_not_own_property(report, 'debug_key'); | ||
} | ||
|
||
assert_own_property(report, 'aggregation_service_payloads'); | ||
verifyAggregationServicePayloads(report.aggregation_service_payloads, expected_cleartext_payload); | ||
|
||
// Check there are no extra keys | ||
assert_equals(Object.keys(report).length, 2); | ||
}; | ||
|
||
/** | ||
* Verifies that two reports are identical except for the payload (which is | ||
* encrypted and thus non-deterministic). Assumes that reports are well formed, | ||
* so should only be called after verifyReport(). | ||
*/ | ||
const verifyReportsIdenticalExceptPayload = (report_a, report_b) => { | ||
report_a.aggregation_service_payloads[0].payload = "PAYLOAD"; | ||
report_b.aggregation_service_payloads[0].payload = "PAYLOAD"; | ||
|
||
assert_equals(JSON.stringify(report_a), JSON.stringify(report_b)); | ||
} |
65 changes: 65 additions & 0 deletions
65
...y/blink/web_tests/wpt_internal/private-aggregation/shared-storage-sends-report.https.html
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
<!doctype html> | ||
<script src="/resources/testharness.js"></script> | ||
<script src="/resources/testharnessreport.js"></script> | ||
<script src="resources/utils.js"></script> | ||
<script src="/shared-storage/resources/util.js"></script> | ||
|
||
<body> | ||
<script> | ||
'use strict'; | ||
|
||
// Payload with contributions [{bucket: 1n, value: 2}] | ||
const ONE_CONTRIBUTION_EXAMPLE_PAYLOAD = | ||
'omRkYXRhgaJldmFsdWVEAAAAAmZidWNrZXRQAAAAAAAAAAAAAAAAAAAAAWlvcGVyYXRpb25paGlzdG9ncmFt'; | ||
|
||
// Payload with contributions [{bucket: 1n, value: 2}, {bucket: 3n, value: 4}] | ||
const MULTIPLE_CONTRIBUTIONS_EXAMPLE_PAYLOAD = | ||
'omRkYXRhgqJldmFsdWVEAAAAAmZidWNrZXRQAAAAAAAAAAAAAAAAAAAAAaJldmFsdWVEAAAABGZidWNrZXRQAAAAAAAAAAAAAAAAAAAAA2lvcGVyYXRpb25paGlzdG9ncmFt'; | ||
|
||
promise_test(async () => { | ||
await addModuleOnce("resources/shared-storage-module.js"); | ||
|
||
const data = {enableDebugMode: true, contributions: [{bucket: 1n, value: 2}]}; | ||
await sharedStorage.run("contribute-to-histogram", {data, keepAlive: true}); | ||
|
||
const reports = await pollReports( | ||
"/.well-known/private-aggregation/report-shared-storage") | ||
assert_equals(reports.length, 1); | ||
|
||
const report = JSON.parse(reports[0]); | ||
verifyReport(report, /*is_debug_enabled=*/true, /*debug_key=*/undefined, | ||
/*expected_cleartext_payload=*/ONE_CONTRIBUTION_EXAMPLE_PAYLOAD); | ||
|
||
const debug_reports = await pollReports( | ||
"/.well-known/private-aggregation/debug/report-shared-storage") | ||
assert_equals(debug_reports.length, 1); | ||
|
||
verifyReportsIdenticalExceptPayload(report, JSON.parse(debug_reports[0])); | ||
|
||
}, 'run() that calls Private Aggregation with one contribution'); | ||
|
||
promise_test(async () => { | ||
await addModuleOnce("resources/shared-storage-module.js"); | ||
|
||
const data = {enableDebugMode: true, | ||
contributions: [{bucket: 1n, value: 2}, {bucket: 3n, value: 4}]}; | ||
|
||
await sharedStorage.run("contribute-to-histogram", {data, keepAlive: true}); | ||
|
||
const reports = await pollReports( | ||
"/.well-known/private-aggregation/report-shared-storage") | ||
assert_equals(reports.length, 1); | ||
|
||
const report = JSON.parse(reports[0]); | ||
verifyReport(report, /*is_debug_enabled=*/true, /*debug_key=*/undefined, | ||
/*expected_cleartext_payload=*/MULTIPLE_CONTRIBUTIONS_EXAMPLE_PAYLOAD); | ||
|
||
const debug_reports = await pollReports( | ||
"/.well-known/private-aggregation/debug/report-shared-storage") | ||
assert_equals(debug_reports.length, 1); | ||
|
||
verifyReportsIdenticalExceptPayload(report, JSON.parse(debug_reports[0])); | ||
}, 'run() that calls Private Aggregation with multiple contributions'); | ||
|
||
</script> | ||
</body> |