Skip to content

Commit

Permalink
Add basic end-to-end Private Aggregation Web Platform Tests
Browse files Browse the repository at this point in the history
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
alexmturner authored and Chromium LUCI CQ committed Jun 16, 2023
1 parent da4efac commit 540a46b
Show file tree
Hide file tree
Showing 8 changed files with 304 additions and 6 deletions.
11 changes: 8 additions & 3 deletions third_party/blink/web_tests/VirtualTestSuites
Original file line number Diff line number Diff line change
Expand Up @@ -1490,13 +1490,18 @@
"prefix": "private-aggregation",
"platforms": ["Linux"],
"bases": [
"external/wpt/private-aggregation"
"external/wpt/private-aggregation",
"wpt_internal/private-aggregation"
],
"exclusive_tests": [
"external/wpt/private-aggregation"
"external/wpt/private-aggregation",
"wpt_internal/private-aggregation"
],
"args": [
"--enable-features=SharedStorageAPI,FencedFrames:implementation_type/mparch,PrivacySandboxAdsAPIsOverride,FencedFramesAPIChanges,FencedFramesDefaultMode,PrivateAggregationApi"],
"--enable-privacy-sandbox-ads-apis",
"--private-aggregation-developer-mode",
"--enable-features=FencedFramesDefaultMode,PrivacySandboxAggregationService:trusted_server_url/https%3A%2F%2Fweb-platform.test%3A8444%2Fwpt_internal%2Fattribution-reporting%2Fresources%2FpublicKeys"
],
"expires": "Jan 1, 2024"
},
{
Expand Down
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)
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)
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."
})
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
The tests are run with the flag:

```
--enable-features=SharedStorageAPI,FencedFrames:implementation_type/mparch,PrivacySandboxAdsAPIsOverride,FencedFramesAPIChanges,FencedFramesDefaultMode,PrivateAggregationApi
--enable-privacy-sandbox-ads-apis
```

You can run these tests by targeting the following directory:
`virtual/private-aggregation/external/wpt/private-aggregation.
You can run these tests by targeting the following directories:
`virtual/private-aggregation/external/wpt/private-aggregation` and
`virtual/private-aggregation/wpt_internal/private-aggregation`.
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);
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));
}
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>

0 comments on commit 540a46b

Please sign in to comment.