summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/attribution-reporting
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/attribution-reporting
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/attribution-reporting')
-rw-r--r--testing/web-platform/tests/attribution-reporting/__init__.py0
-rw-r--r--testing/web-platform/tests/attribution-reporting/aggregatable-report-no-contributions.sub.https.html51
-rw-r--r--testing/web-platform/tests/attribution-reporting/request-format.sub.https.html66
-rw-r--r--testing/web-platform/tests/attribution-reporting/resources/__init__.py0
-rw-r--r--testing/web-platform/tests/attribution-reporting/resources/helpers.js355
-rw-r--r--testing/web-platform/tests/attribution-reporting/resources/reporting_origin.py65
-rw-r--r--testing/web-platform/tests/attribution-reporting/resources/reports.py147
-rw-r--r--testing/web-platform/tests/attribution-reporting/simple-verbose-debug-report.sub.https.html32
8 files changed, 716 insertions, 0 deletions
diff --git a/testing/web-platform/tests/attribution-reporting/__init__.py b/testing/web-platform/tests/attribution-reporting/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/attribution-reporting/__init__.py
diff --git a/testing/web-platform/tests/attribution-reporting/aggregatable-report-no-contributions.sub.https.html b/testing/web-platform/tests/attribution-reporting/aggregatable-report-no-contributions.sub.https.html
new file mode 100644
index 0000000000..b42a61b7bd
--- /dev/null
+++ b/testing/web-platform/tests/attribution-reporting/aggregatable-report-no-contributions.sub.https.html
@@ -0,0 +1,51 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name=timeout content=long>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/attribution-reporting/resources/helpers.js"></script>
+<script>
+attribution_reporting_promise_test(async t => {
+ const host = 'https://{{host}}';
+
+ const expectedSourceEventId = generateSourceEventId();
+ const expectedSourceDebugKey = '456';
+ const expectedTriggerDebugKey = '654';
+
+ registerAttributionSrcByImg(createRedirectChain([
+ {
+ cookie: attributionDebugCookie,
+ source: {
+ aggregation_keys: {
+ campaignCounts: '0x159',
+ },
+ debug_key: expectedSourceDebugKey,
+ destination: host,
+ source_event_id: expectedSourceEventId,
+ },
+ },
+ {
+ trigger : {
+ aggregatable_values: {
+ geoValue: 32768,
+ },
+ debug_key: expectedTriggerDebugKey,
+ debug_reporting: true,
+ },
+ },
+ ]));
+
+ const debugPayload = await pollVerboseDebugReports();
+ assert_equals(debugPayload.reports.length, 1);
+ const debugReport = JSON.parse(debugPayload.reports[0].body);
+ assert_equals(debugReport.length, 1);
+ assert_equals(debugReport[0].type, 'trigger-aggregate-no-contributions');
+ assert_own_property(debugReport[0], 'body');
+ const debugReportBody = debugReport[0].body;
+ assert_equals(debugReportBody.attribution_destination, host);
+ assert_equals(debugReportBody.source_event_id, expectedSourceEventId);
+ assert_equals(debugReportBody.source_site, host);
+ assert_equals(debugReportBody.source_debug_key, expectedSourceDebugKey);
+ assert_equals(debugReportBody.trigger_debug_key, expectedTriggerDebugKey);
+}, 'Aggregatable report is not created due to no contributions.');
+</script>
diff --git a/testing/web-platform/tests/attribution-reporting/request-format.sub.https.html b/testing/web-platform/tests/attribution-reporting/request-format.sub.https.html
new file mode 100644
index 0000000000..a9e36dd126
--- /dev/null
+++ b/testing/web-platform/tests/attribution-reporting/request-format.sub.https.html
@@ -0,0 +1,66 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name=timeout content=long>
+<meta name=variant content="?method=a&expected-eligible=navigation-source">
+<meta name=variant content="?method=img&expected-eligible=event-source, trigger">
+<meta name=variant content="?method=img&eligible&expected-eligible=event-source, trigger">
+<meta name=variant content="?method=open&expected-eligible=navigation-source">
+<meta name=variant content="?method=script&expected-eligible=event-source, trigger">
+<meta name=variant content="?method=script&eligible&expected-eligible=event-source, trigger">
+<meta name=variant content="?method=fetch">
+<meta name=variant content='?method=fetch&eligible={"eventSourceEligible":true,"triggerEligible":false}&expected-eligible=event-source'>
+<meta name=variant content="?method=xhr">
+<meta name=variant content='?method=xhr&eligible={"eventSourceEligible":true,"triggerEligible":false}&expected-eligible=event-source'>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="resources/helpers.js"></script>
+<body>
+<script>
+const waitForRequest = async () => {
+ const url = blankURL();
+ url.searchParams.set('get-requests', 'true');
+
+ for (let i = 0; i < 20; i++) {
+ const resp = await fetch(url);
+ const payload = await resp.json();
+ if (payload !== null && payload.length > 0) {
+ return payload;
+ }
+ await delay(100);
+ }
+ throw new Error('Timeout polling requests');
+};
+
+const searchParams = new URLSearchParams(location.search);
+const expected_eligible =
+ searchParams.get('expected-eligible') === null ? undefined : searchParams.get('expected-eligible');
+
+promise_test(async t => {
+ // Set mixed-case query params to ensure that they are propagated correctly.
+ await registerAttributionSrc({
+ method: 'variant',
+ extraQueryParams: {'aB': 'Cd', 'store-request': 'true'},
+ });
+
+ const requests = await waitForRequest();
+ assert_equals(requests.length, 1);
+ assert_equals(requests[0].method, 'GET');
+ // TODO(apaseltiner): Check header values once WPT can parse structured dictionaries.
+ if (expected_eligible) {
+ assert_own_property(requests[0], 'attribution-reporting-eligible');
+ } else {
+ assert_not_own_property(requests[0], 'attribution-reporting-eligible');
+ }
+ assert_equals(requests[0].referer, location.toString());
+
+ // TODO(apaseltiner): Test various referrer policies.
+ // TODO(apaseltiner): Test cookie propagation.
+
+ const expectedURL = blankURL();
+ expectedURL.searchParams.set('aB', 'Cd');
+ expectedURL.searchParams.set('store-request', 'true');
+ assert_equals(requests[0].url, expectedURL.toString());
+}, 'attributionsrc request has the proper format.');
+</script>
diff --git a/testing/web-platform/tests/attribution-reporting/resources/__init__.py b/testing/web-platform/tests/attribution-reporting/resources/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/attribution-reporting/resources/__init__.py
diff --git a/testing/web-platform/tests/attribution-reporting/resources/helpers.js b/testing/web-platform/tests/attribution-reporting/resources/helpers.js
new file mode 100644
index 0000000000..5778159d4c
--- /dev/null
+++ b/testing/web-platform/tests/attribution-reporting/resources/helpers.js
@@ -0,0 +1,355 @@
+/**
+ * Helper functions for attribution reporting API tests.
+ */
+
+const blankURL = (base = location.origin) => new URL('/attribution-reporting/resources/reporting_origin.py', base);
+
+const attribution_reporting_promise_test = (f, name) =>
+ promise_test(async t => {
+ await resetWptServer();
+ return f(t);
+ }, name);
+
+const resetWptServer = () =>
+ Promise
+ .all([
+ resetAttributionReports(eventLevelReportsUrl),
+ resetAttributionReports(aggregatableReportsUrl),
+ resetAttributionReports(eventLevelDebugReportsUrl),
+ resetAttributionReports(aggregatableDebugReportsUrl),
+ resetAttributionReports(verboseDebugReportsUrl),
+ resetRegisteredSources(),
+ ]);
+
+const eventLevelReportsUrl =
+ '/.well-known/attribution-reporting/report-event-attribution';
+const eventLevelDebugReportsUrl =
+ '/.well-known/attribution-reporting/debug/report-event-attribution';
+const aggregatableReportsUrl =
+ '/.well-known/attribution-reporting/report-aggregate-attribution';
+const aggregatableDebugReportsUrl =
+ '/.well-known/attribution-reporting/debug/report-aggregate-attribution';
+const verboseDebugReportsUrl =
+ '/.well-known/attribution-reporting/debug/verbose';
+
+const attributionDebugCookie = 'ar_debug=1;Secure;HttpOnly;SameSite=None;Path=/';
+
+const pipeHeaderPattern = /[,)]/g;
+
+// , and ) in pipe values must be escaped with \
+const encodeForPipe = urlString => urlString.replace(pipeHeaderPattern, '\\$&');
+
+const blankURLWithHeaders = (headers, origin, status) => {
+ const url = blankURL(origin);
+
+ const parts = headers.map(h => `header(${h.name},${encodeForPipe(h.value)})`);
+
+ if (status !== undefined) {
+ parts.push(`status(${encodeForPipe(status)})`);
+ }
+
+ if (parts.length > 0) {
+ url.searchParams.set('pipe', parts.join('|'));
+ }
+
+ return url;
+};
+
+/**
+ * Clears the source registration stash.
+ */
+const resetRegisteredSources = () => {
+ return fetch(`${blankURL()}?clear-stash=true`);
+}
+
+/**
+ * Method to clear the stash. Takes the URL as parameter. This could be for
+ * event-level or aggregatable reports.
+ */
+const resetAttributionReports = url => {
+ // The view of the stash is path-specific (https://web-platform-tests.org/tools/wptserve/docs/stash.html),
+ // therefore the origin doesn't need to be specified.
+ url = `${url}?clear_stash=true`;
+ const options = {
+ method: 'POST',
+ };
+ return fetch(url, options);
+};
+
+const redirectReportsTo = origin => {
+ return Promise.all([
+ fetch(`${eventLevelReportsUrl}?redirect_to=${origin}`, {method: 'POST'}),
+ fetch(`${aggregatableReportsUrl}?redirect_to=${origin}`, {method: 'POST'})
+ ]);
+};
+
+const getFetchParams = (origin, cookie) => {
+ let credentials;
+ const headers = [];
+
+ if (!origin || origin === location.origin) {
+ return {credentials, headers};
+ }
+
+ // https://fetch.spec.whatwg.org/#http-cors-protocol
+
+ const allowOriginHeader = 'Access-Control-Allow-Origin';
+
+ if (cookie) {
+ credentials = 'include';
+ headers.push({
+ name: 'Access-Control-Allow-Credentials',
+ value: 'true',
+ });
+ headers.push({
+ name: allowOriginHeader,
+ value: `${location.origin}`,
+ });
+ } else {
+ headers.push({
+ name: allowOriginHeader,
+ value: '*',
+ });
+ }
+ return {credentials, headers};
+};
+
+const getDefaultReportingOrigin = () => {
+ // cross-origin means that the reporting origin differs from the source/destination origin.
+ const crossOrigin = new URLSearchParams(location.search).get('cross-origin');
+ return crossOrigin === null ? location.origin : get_host_info().HTTPS_REMOTE_ORIGIN;
+};
+
+const createRedirectChain = (redirects) => {
+ let redirectTo;
+
+ for (let i = redirects.length - 1; i >= 0; i--) {
+ const {source, trigger, cookie, reportingOrigin} = redirects[i];
+ const headers = [];
+
+ if (source) {
+ headers.push({
+ name: 'Attribution-Reporting-Register-Source',
+ value: JSON.stringify(source),
+ });
+ }
+
+ if (trigger) {
+ headers.push({
+ name: 'Attribution-Reporting-Register-Trigger',
+ value: JSON.stringify(trigger),
+ });
+ }
+
+ if (cookie) {
+ headers.push({name: 'Set-Cookie', value: cookie});
+ }
+
+ let status;
+ if (redirectTo) {
+ headers.push({name: 'Location', value: redirectTo.toString()});
+ status = '302';
+ }
+
+ redirectTo = blankURLWithHeaders(
+ headers, reportingOrigin || getDefaultReportingOrigin(), status);
+ }
+
+ return redirectTo;
+};
+
+const registerAttributionSrcByImg = (attributionSrc) => {
+ const element = document.createElement('img');
+ element.attributionSrc = attributionSrc;
+};
+
+const registerAttributionSrc = async ({
+ source,
+ trigger,
+ cookie,
+ method = 'img',
+ extraQueryParams = {},
+ reportingOrigin,
+}) => {
+ const searchParams = new URLSearchParams(location.search);
+
+ if (method === 'variant') {
+ method = searchParams.get('method');
+ }
+
+ const eligible = searchParams.get('eligible');
+
+ let headers = [];
+
+ if (source) {
+ headers.push({
+ name: 'Attribution-Reporting-Register-Source',
+ value: JSON.stringify(source),
+ });
+ }
+
+ if (trigger) {
+ headers.push({
+ name: 'Attribution-Reporting-Register-Trigger',
+ value: JSON.stringify(trigger),
+ });
+ }
+
+ if (cookie) {
+ const name = 'Set-Cookie';
+ headers.push({name, value: cookie});
+ }
+
+
+ let credentials;
+ if (method === 'fetch') {
+ const params = getFetchParams(reportingOrigin, cookie);
+ credentials = params.credentials;
+ headers = headers.concat(params.headers);
+ }
+
+ const url = blankURLWithHeaders(headers, reportingOrigin);
+
+ Object.entries(extraQueryParams)
+ .forEach(([key, value]) => url.searchParams.set(key, value));
+
+ switch (method) {
+ case 'img':
+ const img = document.createElement('img');
+ if (eligible === null) {
+ img.attributionSrc = url;
+ } else {
+ await new Promise(resolve => {
+ img.onload = resolve;
+ // Since the resource being fetched isn't a valid image, onerror will
+ // be fired, but the browser will still process the
+ // attribution-related headers, so resolve the promise instead of
+ // rejecting.
+ img.onerror = resolve;
+ img.attributionSrc = '';
+ img.src = url;
+ });
+ }
+ return 'event';
+ case 'script':
+ const script = document.createElement('script');
+ if (eligible === null) {
+ script.attributionSrc = url;
+ } else {
+ await new Promise(resolve => {
+ script.onload = resolve;
+ script.attributionSrc = '';
+ script.src = url;
+ document.body.appendChild(script);
+ });
+ }
+ return 'event';
+ case 'a':
+ const a = document.createElement('a');
+ a.target = '_blank';
+ a.textContent = 'link';
+ if (eligible === null) {
+ a.attributionSrc = url;
+ a.href = blankURL();
+ } else {
+ a.attributionSrc = '';
+ a.href = url;
+ }
+ document.body.appendChild(a);
+ await test_driver.click(a);
+ return 'navigation';
+ case 'open':
+ await test_driver.bless('open window', () => {
+ if (eligible === null) {
+ open(
+ blankURL(), '_blank',
+ `attributionsrc=${encodeURIComponent(url)}`);
+ } else {
+ open(url, '_blank', 'attributionsrc');
+ }
+ });
+ return 'navigation';
+ case 'fetch': {
+ let attributionReporting;
+ if (eligible !== null) {
+ attributionReporting = JSON.parse(eligible);
+ }
+ await fetch(url, {credentials, attributionReporting});
+ return 'event';
+ }
+ case 'xhr':
+ await new Promise((resolve, reject) => {
+ const req = new XMLHttpRequest();
+ req.open('GET', url);
+ if (eligible !== null) {
+ req.setAttributionReporting(JSON.parse(eligible));
+ }
+ req.onload = resolve;
+ req.onerror = () => reject(req.statusText);
+ req.send();
+ });
+ return 'event';
+ default:
+ throw `unknown method "${method}"`;
+ }
+};
+
+
+/**
+ * Generates a random pseudo-unique source event id.
+ */
+const generateSourceEventId = () => {
+ return `${Math.round(Math.random() * 10000000000000)}`;
+}
+
+/**
+ * Delay method that waits for prescribed number of milliseconds.
+ */
+const delay = ms => new Promise(resolve => step_timeout(resolve, ms));
+
+/**
+ * Method that polls a particular URL for reports. Once reports
+ * are received, returns the payload as promise. Returns null if the
+ * timeout is reached before a report is available.
+ */
+const pollAttributionReports = 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.reports.length > 0) {
+ return payload;
+ }
+ await delay(/*ms=*/ 100);
+ }
+ return null;
+};
+
+// Verbose debug reporting must have been enabled on the source registration for this to work.
+const waitForSourceToBeRegistered = async (sourceId, reportingOrigin) => {
+ const debugReportPayload = await pollVerboseDebugReports(reportingOrigin);
+ assert_equals(debugReportPayload.reports.length, 1);
+ const debugReport = JSON.parse(debugReportPayload.reports[0].body);
+ assert_equals(debugReport.length, 1);
+ assert_equals(debugReport[0].type, 'source-success');
+ assert_equals(debugReport[0].body.source_event_id, sourceId);
+};
+
+const pollEventLevelReports = (origin) =>
+ pollAttributionReports(eventLevelReportsUrl, origin);
+const pollEventLevelDebugReports = (origin) =>
+ pollAttributionReports(eventLevelDebugReportsUrl, origin);
+const pollAggregatableReports = (origin) =>
+ pollAttributionReports(aggregatableReportsUrl, origin);
+const pollAggregatableDebugReports = (origin) =>
+ pollAttributionReports(aggregatableDebugReportsUrl, origin);
+const pollVerboseDebugReports = (origin) =>
+ pollAttributionReports(verboseDebugReportsUrl, origin);
+
+const validateReportHeaders = headers => {
+ assert_array_equals(headers['content-type'], ['application/json']);
+ assert_array_equals(headers['cache-control'], ['no-cache']);
+ assert_own_property(headers, 'user-agent');
+ assert_not_own_property(headers, 'cookie');
+ assert_not_own_property(headers, 'referer');
+};
diff --git a/testing/web-platform/tests/attribution-reporting/resources/reporting_origin.py b/testing/web-platform/tests/attribution-reporting/resources/reporting_origin.py
new file mode 100644
index 0000000000..4a5877035e
--- /dev/null
+++ b/testing/web-platform/tests/attribution-reporting/resources/reporting_origin.py
@@ -0,0 +1,65 @@
+"""Test reporting origin server used for two reasons:
+
+ 1. It is a workaround for lack of preflight support in the test server.
+ 2. Stashes requests so they can be inspected by tests.
+"""
+
+from wptserve.stash import Stash
+import json
+
+REQUESTS = "9250f93f-2c05-4aae-83b9-2817b0e18b4d"
+
+
+headers = [
+ b"attribution-reporting-eligible",
+ b"attribution-reporting-support",
+ b"referer",
+]
+
+
+def store_request(request) -> None:
+ obj = {
+ "method": request.method,
+ "url": request.url,
+ }
+ for header in headers:
+ value = request.headers.get(header)
+ if value is not None:
+ obj[str(header, "utf-8")] = str(value, "utf-8")
+ with request.server.stash.lock:
+ requests = request.server.stash.take(REQUESTS)
+ if not requests:
+ requests = []
+ requests.append(obj)
+ request.server.stash.put(REQUESTS, requests)
+ return None
+
+
+def get_requests(request) -> str:
+ with request.server.stash.lock:
+ return json.dumps(request.server.stash.take(REQUESTS))
+
+
+def main(request, response):
+ """
+ For most requests, simply returns a 200. Actual source/trigger registration
+ headers are piped using the `pipe` query param.
+
+ If a `clear-stash` param is set, it will clear the stash.
+ """
+ if request.GET.get(b"clear-stash"):
+ request.stash.take(REQUESTS)
+ return
+
+ # We dont want to redirect preflight requests. The cors headers are piped
+ # so we can simply return a 200 and redirect the following request
+ if request.method == "OPTIONS":
+ response.status = 200
+ return
+
+ if request.GET.get(b"get-requests"):
+ return get_requests(request)
+
+ if request.GET.get(b"store-request"):
+ store_request(request)
+ return ""
diff --git a/testing/web-platform/tests/attribution-reporting/resources/reports.py b/testing/web-platform/tests/attribution-reporting/resources/reports.py
new file mode 100644
index 0000000000..b71743b0fc
--- /dev/null
+++ b/testing/web-platform/tests/attribution-reporting/resources/reports.py
@@ -0,0 +1,147 @@
+"""Methods for the report-event-attribution and report-aggregate-attribution endpoints"""
+import json
+from typing import List, Optional, Tuple
+import urllib.parse
+
+from wptserve.request import Request
+from wptserve.stash import Stash
+from wptserve.utils import isomorphic_decode, isomorphic_encode
+
+# Key used to access the reports in the stash.
+REPORTS = "4691a2d7fca5430fb0f33b1bd8a9d388"
+REDIRECT = "9250f93f-2c05-4aae-83b9-2817b0e18b4e"
+
+CLEAR_STASH = isomorphic_encode("clear_stash")
+CONFIG_REDIRECT = isomorphic_encode("redirect_to")
+
+Header = Tuple[str, str]
+Status = Tuple[int, str]
+Response = Tuple[Status, List[Header], str]
+
+def decode_headers(headers: dict) -> dict:
+ """Decodes the headers from wptserve.
+
+ wptserve headers are encoded like
+ {
+ encoded(key): [encoded(value1), encoded(value2),...]
+ }
+ This method decodes the above using the wptserve.utils.isomorphic_decode
+ method
+ """
+ return {
+ isomorphic_decode(key): [isomorphic_decode(el) for el in value
+ ] for key, value in headers.items()
+ }
+
+def get_request_origin(request: Request) -> str:
+ return "%s://%s" % (request.url_parts.scheme,
+ request.url_parts.netloc)
+
+def configure_redirect(request, origin) -> None:
+ with request.server.stash.lock:
+ request.server.stash.put(REDIRECT, origin)
+ return None
+
+def get_report_redirect_url(request):
+ with request.server.stash.lock:
+ origin = request.server.stash.take(REDIRECT)
+ if origin is None:
+ return None
+ origin_parts = urllib.parse.urlsplit(origin)
+ parts = request.url_parts
+ new_parts = origin_parts._replace(path=bytes(parts.path, 'utf-8'))
+ return urllib.parse.urlunsplit(new_parts)
+
+def handle_post_report(request: Request, headers: List[Header]) -> 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):
+ clear_stash(request.server.stash)
+ return (200, "OK"), headers, json.dumps({
+ "code": 200,
+ "message": "Stash successfully cleared.",
+ })
+
+ redirect_origin = request.GET.get(CONFIG_REDIRECT)
+ if redirect_origin:
+ configure_redirect(request, redirect_origin)
+ return (200, "OK"), headers, json.dumps({
+ "code": 200,
+ "message": "Redirect successfully configured.",
+ })
+
+ redirect_url = get_report_redirect_url(request)
+ if redirect_url is not None:
+ headers.append(("Location", redirect_url))
+ return (308, "Permanent Redirect"), headers, json.dumps({
+ "code": 308
+ })
+
+ store_report(
+ request.server.stash, get_request_origin(request), {
+ "body": request.body.decode("utf-8"),
+ "headers": decode_headers(request.headers)
+ })
+ return (201, "OK"), headers, json.dumps({
+ "code": 201,
+ "message": "Report successfully stored."
+ })
+
+
+def handle_get_reports(request: Request, headers: List[Header]) -> Response:
+ """Handles GET request for reports.
+
+ Retrieves and returns all reports from the stash.
+ """
+ reports = take_reports(request.server.stash, get_request_origin(request))
+ headers.append(("Access-Control-Allow-Origin", "*"))
+ return (200, "OK"), headers, json.dumps({
+ "code": 200,
+ "reports": 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)
+ if not reports_dict:
+ reports_dict = {}
+ reports = reports_dict.get(origin, [])
+ reports.append(report)
+ reports_dict[origin] = reports
+ stash.put(REPORTS, reports_dict)
+ return None
+
+def clear_stash(stash: Stash) -> None:
+ "Clears the stash."
+ stash.take(REPORTS)
+ stash.take(REDIRECT)
+ 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)
+ if not reports_dict:
+ reports_dict = {}
+
+ reports = reports_dict.pop(origin, [])
+ stash.put(REPORTS, reports_dict)
+ return reports
+
+
+def handle_reports(request: Request) -> Response:
+ """Handles request to get or store reports."""
+ headers = [("Content-Type", "application/json")]
+ if request.method == "POST":
+ return handle_post_report(request, headers)
+ if request.method == "GET":
+ return handle_get_reports(request, headers)
+ return (405, "Method Not Allowed"), headers, json.dumps({
+ "code": 405,
+ "message": "Only GET or POST methods are supported."
+ })
diff --git a/testing/web-platform/tests/attribution-reporting/simple-verbose-debug-report.sub.https.html b/testing/web-platform/tests/attribution-reporting/simple-verbose-debug-report.sub.https.html
new file mode 100644
index 0000000000..8a477f732f
--- /dev/null
+++ b/testing/web-platform/tests/attribution-reporting/simple-verbose-debug-report.sub.https.html
@@ -0,0 +1,32 @@
+<!doctype html>
+<meta charset=utf-8>
+<meta name=timeout content=long>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/attribution-reporting/resources/helpers.js"></script>
+<script>
+attribution_reporting_promise_test(async t => {
+ const expectedTriggerDebugKey = '456';
+
+ registerAttributionSrcByImg(createRedirectChain([
+ {
+ cookie: attributionDebugCookie,
+ trigger: {
+ debug_reporting: true,
+ debug_key: expectedTriggerDebugKey,
+ event_trigger_data: [{}],
+ },
+ },
+ ]));
+
+ const payload = await pollVerboseDebugReports();
+ assert_equals(payload.reports.length, 1);
+ const report = JSON.parse(payload.reports[0].body);
+ assert_equals(report.length, 1);
+ assert_equals(report[0].type, 'trigger-no-matching-source');
+ assert_own_property(report[0], 'body');
+ assert_equals(report[0].body.attribution_destination, 'https://{{host}}');
+ assert_equals(report[0].body.trigger_debug_key, expectedTriggerDebugKey);
+}, 'Verbose debug report is received.');
+</script>