blob: b2c69fc3f5ab15ae356ef85e320099af580591c5 [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/attribution_reporting/attribution_internals_ui.h"
#include <stdint.h>
#include <limits>
#include <utility>
#include <vector>
#include "base/callback.h"
#include "base/command_line.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "content/browser/attribution_reporting/attribution_aggregatable_trigger_data.h"
#include "content/browser/attribution_reporting/attribution_aggregatable_values.h"
#include "content/browser/attribution_reporting/attribution_aggregation_keys.h"
#include "content/browser/attribution_reporting/attribution_filter_data.h"
#include "content/browser/attribution_reporting/attribution_manager.h"
#include "content/browser/attribution_reporting/attribution_observer_types.h"
#include "content/browser/attribution_reporting/attribution_report.h"
#include "content/browser/attribution_reporting/attribution_source_type.h"
#include "content/browser/attribution_reporting/attribution_test_utils.h"
#include "content/browser/attribution_reporting/attribution_trigger.h"
#include "content/browser/attribution_reporting/send_result.h"
#include "content/browser/attribution_reporting/storable_source.h"
#include "content/browser/attribution_reporting/stored_source.h"
#include "content/browser/storage_partition_impl.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/test_utils.h"
#include "content/shell/browser/shell.h"
#include "net/base/net_errors.h"
#include "testing/gmock/include/gmock/gmock.h"
namespace content {
namespace {
using ::testing::_;
using ::testing::ElementsAre;
using ::testing::IsNull;
using ::testing::Return;
using ::testing::VariantWith;
const char kAttributionInternalsUrl[] = "chrome://attribution-internals/";
const std::u16string kCompleteTitle = u"Complete";
const std::u16string kCompleteTitle2 = u"Complete2";
const std::u16string kCompleteTitle3 = u"Complete3";
const std::u16string kMaxInt64String = u"9223372036854775807";
const std::u16string kMaxUint64String = u"18446744073709551615";
auto InvokeCallback(std::vector<StoredSource> value) {
return [value = std::move(value)](
base::OnceCallback<void(std::vector<StoredSource>)> callback) {
std::move(callback).Run(std::move(value));
};
}
auto InvokeCallback(std::vector<AttributionReport> value) {
return
[value = std::move(value)](
AttributionReport::ReportTypes report_types, int limit,
base::OnceCallback<void(std::vector<AttributionReport>)> callback) {
std::move(callback).Run(std::move(value));
};
}
AttributionReport IrreleventEventLevelReport() {
return ReportBuilder(
AttributionInfoBuilder(SourceBuilder().BuildStored()).Build())
.Build();
}
AttributionReport IrreleventAggregatableReport() {
return ReportBuilder(
AttributionInfoBuilder(SourceBuilder().BuildStored()).Build())
.SetAggregatableHistogramContributions(
{AggregatableHistogramContribution(1, 2)})
.BuildAggregatableAttribution();
}
} // namespace
class AttributionInternalsWebUiBrowserTest : public ContentBrowserTest {
public:
AttributionInternalsWebUiBrowserTest() = default;
void SetUpOnMainThread() override {
ContentBrowserTest::SetUpOnMainThread();
auto manager = std::make_unique<MockAttributionManager>();
manager_ = manager.get();
ON_CALL(*manager_, GetActiveSourcesForWebUI)
.WillByDefault(InvokeCallback(std::vector<StoredSource>{}));
ON_CALL(*manager_, GetPendingReportsForInternalUse)
.WillByDefault(InvokeCallback(std::vector<AttributionReport>{}));
static_cast<StoragePartitionImpl*>(shell()
->web_contents()
->GetBrowserContext()
->GetDefaultStoragePartition())
->OverrideAttributionManagerForTesting(std::move(manager));
}
void ClickRefreshButton() {
EXPECT_TRUE(ExecJsInWebUI("document.getElementById('refresh').click();"));
}
// Executing javascript in the WebUI requires using an isolated world in which
// to execute the script because WebUI has a default CSP policy denying
// "eval()", which is what EvalJs uses under the hood.
bool ExecJsInWebUI(const std::string& script) {
return ExecJs(shell()->web_contents()->GetPrimaryMainFrame(), script,
EXECUTE_SCRIPT_DEFAULT_OPTIONS, /*world_id=*/1);
}
// Registers a mutation observer that sets the window title to |title| when
// the report table is empty.
void SetTitleOnReportsTableEmpty(const std::u16string& title) {
static constexpr char kObserveEmptyReportsTableScript[] = R"(
let table = document.querySelector('#reportTable')
.shadowRoot.querySelector('tbody');
let obs = new MutationObserver((_, obs) => {
if (table.children.length === 1 &&
table.children[0].children[0].innerText === "No sent or pending reports.") {
obs.disconnect();
document.title = $1;
}
});
obs.observe(table, {'childList': true});)";
EXPECT_TRUE(
ExecJsInWebUI(JsReplace(kObserveEmptyReportsTableScript, title)));
}
MockAttributionManager* manager() { return manager_; }
private:
raw_ptr<MockAttributionManager, DanglingUntriaged> manager_;
};
IN_PROC_BROWSER_TEST_F(AttributionInternalsWebUiBrowserTest,
NavigationUrl_ResolvedToWebUI) {
EXPECT_TRUE(NavigateToURL(shell(), GURL(kAttributionInternalsUrl)));
// Execute script to ensure the page has loaded correctly, executing similarly
// to ExecJsInWebUI().
EXPECT_EQ(true, EvalJs(shell()->web_contents()->GetPrimaryMainFrame(),
"document.body.innerHTML.search('Attribution "
"Reporting API Internals') >= 0;",
EXECUTE_SCRIPT_DEFAULT_OPTIONS, /*world_id=*/1));
}
IN_PROC_BROWSER_TEST_F(AttributionInternalsWebUiBrowserTest,
WebUIShownWithManager_MeasurementConsideredEnabled) {
EXPECT_TRUE(NavigateToURL(shell(), GURL(kAttributionInternalsUrl)));
// Create a mutation observer to wait for the content to render to the dom.
// Waiting on calls to `MockAttributionManager` is not sufficient because the
// results are returned in promises.
static constexpr char wait_script[] = R"(
let status = document.getElementById("feature-status-content");
let obs = new MutationObserver((_, obs) => {
if (status.innerText.trim() === "enabled") {
obs.disconnect();
document.title = $1;
}
});
obs.observe(status, {'childList': true, 'characterData': true});)";
EXPECT_TRUE(ExecJsInWebUI(JsReplace(wait_script, kCompleteTitle)));
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
ClickRefreshButton();
EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle());
}
IN_PROC_BROWSER_TEST_F(AttributionInternalsWebUiBrowserTest,
DisabledByEmbedder_MeasurementConsideredDisabled) {
MockAttributionReportingContentBrowserClient browser_client;
EXPECT_CALL(browser_client,
IsConversionMeasurementOperationAllowed(
_, ContentBrowserClient::ConversionMeasurementOperation::kAny,
IsNull(), IsNull(), IsNull()))
.WillRepeatedly(Return(false));
ScopedContentBrowserClientSetting setting(&browser_client);
EXPECT_TRUE(NavigateToURL(shell(), GURL(kAttributionInternalsUrl)));
// Create a mutation observer to wait for the content to render to the dom.
// Waiting on calls to `MockAttributionManager` is not sufficient because the
// results are returned in promises.
static constexpr char wait_script[] = R"(
let status = document.getElementById("feature-status-content");
let obs = new MutationObserver((_, obs) => {
if (status.innerText.trim() === "disabled") {
obs.disconnect();
document.title = $1;
}
});
obs.observe(status, {'childList': true, 'characterData': true});)";
EXPECT_TRUE(ExecJsInWebUI(JsReplace(wait_script, kCompleteTitle)));
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
ClickRefreshButton();
EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle());
}
IN_PROC_BROWSER_TEST_F(
AttributionInternalsWebUiBrowserTest,
WebUIShownWithNoActiveImpression_NoImpressionsDisplayed) {
EXPECT_TRUE(NavigateToURL(shell(), GURL(kAttributionInternalsUrl)));
static constexpr char wait_script[] = R"(
let table = document.querySelector('#sourceTable')
.shadowRoot.querySelector('tbody');
let obs = new MutationObserver((_, obs) => {
if (table.children.length === 1 &&
table.children[0].children[0].innerText ===
"No sources.") {
obs.disconnect();
document.title = $1;
}
});
obs.observe(table, {'childList': true});)";
EXPECT_TRUE(ExecJsInWebUI(JsReplace(wait_script, kCompleteTitle)));
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
ClickRefreshButton();
EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle());
}
IN_PROC_BROWSER_TEST_F(AttributionInternalsWebUiBrowserTest,
WebUIShownWithActiveImpression_ImpressionsDisplayed) {
EXPECT_TRUE(NavigateToURL(shell(), GURL(kAttributionInternalsUrl)));
const base::Time now = base::Time::Now();
// We use the max values of `uint64_t` and `int64_t` here to ensure that they
// are properly handled as `bigint` values in JS and don't run into issues
// with `Number.MAX_SAFE_INTEGER`.
ON_CALL(*manager(), GetActiveSourcesForWebUI)
.WillByDefault(InvokeCallback(
{SourceBuilder(now)
.SetSourceEventId(std::numeric_limits<uint64_t>::max())
.SetAttributionLogic(StoredSource::AttributionLogic::kNever)
.SetDebugKey(19)
.BuildStored(),
SourceBuilder(now + base::Hours(1))
.SetSourceType(AttributionSourceType::kEvent)
.SetPriority(std::numeric_limits<int64_t>::max())
.SetDedupKeys({13, 17})
.SetFilterData(*AttributionFilterData::FromSourceFilterValues(
{{"a", {"b", "c"}}}))
.SetAggregationKeys(
*AttributionAggregationKeys::FromKeys({{"a", 1}}))
.BuildStored(),
SourceBuilder(now + base::Hours(2))
.SetActiveState(StoredSource::ActiveState::
kReachedEventLevelAttributionLimit)
.BuildStored()}));
manager()->NotifySourceDeactivated(
SourceBuilder(now + base::Hours(3)).BuildStored());
// This shouldn't result in a row, as registration succeeded.
manager()->NotifySourceHandled(SourceBuilder(now).Build(),
StorableSource::Result::kSuccess);
manager()->NotifySourceHandled(SourceBuilder(now + base::Hours(4)).Build(),
StorableSource::Result::kInternalError);
manager()->NotifySourceHandled(
SourceBuilder(now + base::Hours(5)).Build(),
StorableSource::Result::kInsufficientSourceCapacity);
manager()->NotifySourceHandled(
SourceBuilder(now + base::Hours(6)).Build(),
StorableSource::Result::kInsufficientUniqueDestinationCapacity);
manager()->NotifySourceHandled(
SourceBuilder(now + base::Hours(7)).Build(),
StorableSource::Result::kExcessiveReportingOrigins);
static constexpr char wait_script[] = R"(
let table = document.querySelector('#sourceTable')
.shadowRoot.querySelector('tbody');
let obs = new MutationObserver((_, obs) => {
if (table.children.length === 8 &&
table.children[0].children[0].innerText === $1 &&
table.children[0].children[7].innerText === "Navigation" &&
table.children[1].children[7].innerText === "Event" &&
table.children[0].children[8].innerText === "0" &&
table.children[1].children[8].innerText === $2 &&
table.children[0].children[9].innerText === "{}" &&
table.children[1].children[9].innerText === '{\n "a": [\n "b",\n "c"\n ]\n}' &&
table.children[0].children[10].innerText === "{}" &&
table.children[1].children[10].innerText === '{\n "a": "0x1"\n}' &&
table.children[0].children[11].innerText === "19" &&
table.children[1].children[11].innerText === "" &&
table.children[0].children[12].innerText === "" &&
table.children[1].children[12].innerText === "13, 17" &&
table.children[0].children[1].innerText === "Unattributable: noised" &&
table.children[1].children[1].innerText === "Attributable" &&
table.children[2].children[1].innerText === "Attributable: reached event-level attribution limit" &&
table.children[3].children[1].innerText === "Unattributable: replaced by newer source" &&
table.children[4].children[1].innerText === "Rejected: internal error" &&
table.children[5].children[1].innerText === "Rejected: insufficient source capacity" &&
table.children[6].children[1].innerText === "Rejected: insufficient unique destination capacity" &&
table.children[7].children[1].innerText === "Rejected: excessive reporting origins") {
obs.disconnect();
document.title = $3;
}
});
obs.observe(table, {'childList': true});)";
EXPECT_TRUE(ExecJsInWebUI(JsReplace(wait_script, kMaxUint64String,
kMaxInt64String, kCompleteTitle)));
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
ClickRefreshButton();
EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle());
}
IN_PROC_BROWSER_TEST_F(AttributionInternalsWebUiBrowserTest,
WebUIShownWithNoReports_NoReportsDisplayed) {
EXPECT_TRUE(NavigateToURL(shell(), GURL(kAttributionInternalsUrl)));
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
SetTitleOnReportsTableEmpty(kCompleteTitle);
ClickRefreshButton();
EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle());
}
IN_PROC_BROWSER_TEST_F(AttributionInternalsWebUiBrowserTest,
WebUIShownWithManager_DebugModeDisabled) {
EXPECT_TRUE(NavigateToURL(shell(), GURL(kAttributionInternalsUrl)));
// Create a mutation observer to wait for the content to render to the dom.
// Waiting on calls to `MockAttributionManager` is not sufficient because the
// results are returned in promises.
static constexpr char wait_script[] = R"(
let status = document.getElementById("debug-mode-content");
let obs = new MutationObserver((_, obs) => {
if (status.innerText.trim() === "") {
obs.disconnect();
document.title = $1;
}
});
obs.observe(status, {'childList': true, 'characterData': true});)";
EXPECT_TRUE(ExecJsInWebUI(JsReplace(wait_script, kCompleteTitle)));
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
ClickRefreshButton();
EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle());
}
IN_PROC_BROWSER_TEST_F(AttributionInternalsWebUiBrowserTest,
WebUIShownWithManager_DebugModeEnabled) {
base::CommandLine::ForCurrentProcess()->AppendSwitch(
switches::kConversionsDebugMode);
EXPECT_TRUE(NavigateToURL(shell(), GURL(kAttributionInternalsUrl)));
// Create a mutation observer to wait for the content to render to the dom.
// Waiting on calls to `MockAttributionManager` is not sufficient because the
// results are returned in promises.
static constexpr char wait_script[] = R"(
let status = document.getElementById("debug-mode-content");
let obs = new MutationObserver((_, obs) => {
if (status.innerText.trim() !== "") {
obs.disconnect();
document.title = $1;
}
});
obs.observe(status, {'childList': true, 'characterData': true});)";
EXPECT_TRUE(ExecJsInWebUI(JsReplace(wait_script, kCompleteTitle)));
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
ClickRefreshButton();
EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle());
}
IN_PROC_BROWSER_TEST_F(AttributionInternalsWebUiBrowserTest,
WebUIShownWithPendingReports_ReportsDisplayed) {
EXPECT_TRUE(NavigateToURL(shell(), GURL(kAttributionInternalsUrl)));
const base::Time now = base::Time::Now();
manager()->NotifyReportSent(
ReportBuilder(
AttributionInfoBuilder(SourceBuilder(now).BuildStored()).Build())
.SetReportTime(now + base::Hours(3))
.Build(),
/*is_debug_report=*/false,
SendResult(SendResult::Status::kSent, net::OK,
/*http_response_code=*/200));
manager()->NotifyReportSent(
ReportBuilder(
AttributionInfoBuilder(SourceBuilder(now).BuildStored()).Build())
.SetReportTime(now + base::Hours(4))
.SetPriority(-1)
.Build(),
/*is_debug_report=*/false, SendResult(SendResult::Status::kDropped));
manager()->NotifyReportSent(
ReportBuilder(
AttributionInfoBuilder(SourceBuilder(now).BuildStored()).Build())
.SetReportTime(now + base::Hours(5))
.SetPriority(-2)
.Build(),
/*is_debug_report=*/false,
SendResult(SendResult::Status::kFailure, net::ERR_METHOD_NOT_SUPPORTED));
manager()->NotifyReportSent(
ReportBuilder(
AttributionInfoBuilder(SourceBuilder(now).BuildStored()).Build())
.SetReportTime(now + base::Hours(11))
.SetPriority(-8)
.Build(),
/*is_debug_report=*/true,
SendResult(SendResult::Status::kTransientFailure, net::ERR_TIMED_OUT));
ON_CALL(*manager(), GetPendingReportsForInternalUse)
.WillByDefault(InvokeCallback(
{ReportBuilder(AttributionInfoBuilder(
SourceBuilder(now)
.SetSourceType(AttributionSourceType::kEvent)
.SetAttributionLogic(
StoredSource::AttributionLogic::kFalsely)
.BuildStored())
.Build())
.SetReportTime(now)
.SetPriority(13)
.Build()}));
manager()->NotifyTriggerHandled(
DefaultTrigger(),
CreateReportResult(
/*trigger_time=*/base::Time::Now(),
AttributionTrigger::EventLevelResult::kSuccessDroppedLowerPriority,
AttributionTrigger::AggregatableResult::kNoHistograms,
/*replaced_event_level_report=*/
ReportBuilder(
AttributionInfoBuilder(SourceBuilder(now).BuildStored()).Build())
.SetReportTime(now + base::Hours(1))
.SetPriority(11)
.Build(),
/*new_event_level_report=*/IrreleventEventLevelReport()));
{
static constexpr char wait_script[] = R"(
let table = document.querySelector('#reportTable')
.shadowRoot.querySelector('tbody');
let obs = new MutationObserver((_, obs) => {
if (table.children.length === 6 &&
table.children[0].children[3].innerText ===
"https://report.test/.well-known/attribution-reporting/report-event-attribution" &&
table.children[0].children[6].innerText === "13" &&
table.children[0].children[7].innerText === "yes" &&
table.children[0].children[2].innerText === "Pending" &&
table.children[1].children[6].innerText === "11" &&
table.children[1].children[2].innerText ===
"Replaced by higher-priority report: 21abd97f-73e8-4b88-9389-a9fee6abda5e" &&
table.children[2].children[6].innerText === "0" &&
table.children[2].children[7].innerText === "no" &&
table.children[2].children[2].innerText === "Sent: HTTP 200" &&
table.children[3].children[2].innerText === "Prohibited by browser policy" &&
table.children[4].children[2].innerText === "Network error: ERR_METHOD_NOT_SUPPORTED" &&
table.children[5].children[2].innerText === "Network error: ERR_TIMED_OUT" &&
table.children[5].children[3].innerText ===
"https://report.test/.well-known/attribution-reporting/debug/report-event-attribution") {
obs.disconnect();
document.title = $1;
}
});
obs.observe(table, {'childList': true});)";
EXPECT_TRUE(ExecJsInWebUI(JsReplace(wait_script, kCompleteTitle)));
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
ClickRefreshButton();
EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle());
}
{
static constexpr char wait_script[] = R"(
let table = document.querySelector('#reportTable')
.shadowRoot.querySelector('tbody');
let obs = new MutationObserver((_, obs) => {
if (table.children.length === 6 &&
table.children[5].children[3].innerText ===
"https://report.test/.well-known/attribution-reporting/report-event-attribution" &&
table.children[5].children[6].innerText === "13" &&
table.children[5].children[7].innerText === "yes" &&
table.children[5].children[2].innerText === "Pending" &&
table.children[4].children[6].innerText === "11" &&
table.children[4].children[2].innerText ===
"Replaced by higher-priority report: 21abd97f-73e8-4b88-9389-a9fee6abda5e" &&
table.children[3].children[6].innerText === "0" &&
table.children[3].children[7].innerText === "no" &&
table.children[3].children[2].innerText === "Sent: HTTP 200" &&
table.children[2].children[2].innerText === "Prohibited by browser policy" &&
table.children[1].children[2].innerText === "Network error: ERR_METHOD_NOT_SUPPORTED" &&
table.children[0].children[2].innerText === "Network error: ERR_TIMED_OUT" &&
table.children[0].children[3].innerText ===
"https://report.test/.well-known/attribution-reporting/debug/report-event-attribution") {
obs.disconnect();
document.title = $1;
}
});
obs.observe(table, {'childList': true});)";
EXPECT_TRUE(ExecJsInWebUI(JsReplace(wait_script, kCompleteTitle2)));
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle2);
// Sort by priority ascending.
EXPECT_TRUE(
ExecJsInWebUI("document.querySelector('#reportTable')"
".shadowRoot.querySelectorAll('th')[6].click();"));
EXPECT_EQ(kCompleteTitle2, title_watcher.WaitAndGetTitle());
}
{
static constexpr char wait_script[] = R"(
let table = document.querySelector('#reportTable')
.shadowRoot.querySelector('tbody');
let obs = new MutationObserver((_, obs) => {
if (table.children.length === 6 &&
table.children[0].children[3].innerText ===
"https://report.test/.well-known/attribution-reporting/report-event-attribution" &&
table.children[0].children[6].innerText === "13" &&
table.children[0].children[7].innerText === "yes" &&
table.children[0].children[2].innerText === "Pending" &&
table.children[1].children[6].innerText === "11" &&
table.children[1].children[2].innerText ===
"Replaced by higher-priority report: 21abd97f-73e8-4b88-9389-a9fee6abda5e" &&
table.children[2].children[6].innerText === "0" &&
table.children[2].children[7].innerText === "no" &&
table.children[2].children[2].innerText === "Sent: HTTP 200" &&
table.children[3].children[2].innerText === "Prohibited by browser policy" &&
table.children[4].children[2].innerText === "Network error: ERR_METHOD_NOT_SUPPORTED" &&
table.children[5].children[2].innerText === "Network error: ERR_TIMED_OUT" &&
table.children[5].children[3].innerText ===
"https://report.test/.well-known/attribution-reporting/debug/report-event-attribution") {
obs.disconnect();
document.title = $1;
}
});
obs.observe(table, {'childList': true});)";
EXPECT_TRUE(ExecJsInWebUI(JsReplace(wait_script, kCompleteTitle3)));
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle3);
// Sort by priority descending.
EXPECT_TRUE(
ExecJsInWebUI("document.querySelector('#reportTable')"
".shadowRoot.querySelectorAll('th')[6].click();"));
EXPECT_EQ(kCompleteTitle3, title_watcher.WaitAndGetTitle());
}
}
IN_PROC_BROWSER_TEST_F(AttributionInternalsWebUiBrowserTest,
WebUIWithPendingReportsClearStorage_ReportsRemoved) {
EXPECT_TRUE(NavigateToURL(shell(), GURL(kAttributionInternalsUrl)));
const base::Time now = base::Time::Now();
AttributionReport report =
ReportBuilder(
AttributionInfoBuilder(SourceBuilder(now).BuildStored()).Build())
.SetReportTime(now)
.SetPriority(7)
.Build();
EXPECT_CALL(*manager(), GetPendingReportsForInternalUse)
.WillOnce(InvokeCallback({report}))
.WillOnce(InvokeCallback(std::vector<AttributionReport>{}));
report.set_report_time(report.report_time() + base::Hours(1));
manager()->NotifyReportSent(report,
/*is_debug_report=*/false,
SendResult(SendResult::Status::kSent, net::OK,
/*http_response_code=*/200));
EXPECT_CALL(*manager(), ClearData)
.WillOnce([](base::Time delete_begin, base::Time delete_end,
StoragePartition::StorageKeyMatcherFunction filter,
bool delete_rate_limit_data,
base::OnceClosure done) { std::move(done).Run(); });
// Verify both rows get rendered.
static constexpr char wait_script[] = R"(
let table = document.querySelector('#reportTable')
.shadowRoot.querySelector('tbody');
let obs = new MutationObserver((_, obs) => {
if (table.children.length === 2 &&
table.children[0].children[6].innerText === "7" &&
table.children[1].children[2].innerText === "Sent: HTTP 200") {
obs.disconnect();
document.title = $1;
}
});
obs.observe(table, {'childList': true});)";
EXPECT_TRUE(ExecJsInWebUI(JsReplace(wait_script, kCompleteTitle)));
// Wait for the table to rendered.
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
ClickRefreshButton();
EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle());
// Click the clear storage button and expect that the report table is emptied.
const std::u16string kDeleteTitle = u"Delete";
TitleWatcher delete_title_watcher(shell()->web_contents(), kDeleteTitle);
SetTitleOnReportsTableEmpty(kDeleteTitle);
// Click the button.
EXPECT_TRUE(ExecJsInWebUI("document.getElementById('clear-data').click();"));
EXPECT_EQ(kDeleteTitle, delete_title_watcher.WaitAndGetTitle());
}
IN_PROC_BROWSER_TEST_F(AttributionInternalsWebUiBrowserTest,
ClearButton_ClearsSourceTable) {
EXPECT_TRUE(NavigateToURL(shell(), GURL(kAttributionInternalsUrl)));
base::Time now = base::Time::Now();
ON_CALL(*manager(), GetActiveSourcesForWebUI)
.WillByDefault(InvokeCallback(
{SourceBuilder(now).SetSourceEventId(5).BuildStored()}));
manager()->NotifySourceDeactivated(
SourceBuilder(now + base::Hours(2)).SetSourceEventId(6).BuildStored());
EXPECT_CALL(*manager(),
ClearData(base::Time::Min(), base::Time::Max(), _, true, _))
.WillOnce([](base::Time delete_begin, base::Time delete_end,
StoragePartition::StorageKeyMatcherFunction filter,
bool delete_rate_limit_data,
base::OnceClosure done) { std::move(done).Run(); });
// Verify both rows get rendered.
static constexpr char wait_script[] = R"(
let table = document.querySelector('#sourceTable')
.shadowRoot.querySelector('tbody');
let obs = new MutationObserver((_, obs) => {
if (table.children.length === 2 &&
table.children[0].children[0].innerText === "5" &&
table.children[1].children[0].innerText === "6") {
obs.disconnect();
document.title = $1;
}
});
obs.observe(table, {'childList': true});)";
EXPECT_TRUE(ExecJsInWebUI(JsReplace(wait_script, kCompleteTitle)));
// Wait for the table to rendered.
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
ClickRefreshButton();
EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle());
// Click the clear storage button and expect that the source table is emptied.
const std::u16string kDeleteTitle = u"Delete";
TitleWatcher delete_title_watcher(shell()->web_contents(), kDeleteTitle);
static constexpr char kObserveEmptySourcesTableScript[] = R"(
let table = document.querySelector('#sourceTable')
.shadowRoot.querySelector('tbody');
let obs = new MutationObserver((_, obs) => {
if (table.children.length === 1 &&
table.children[0].children[0].innerText === "No sources.") {
obs.disconnect();
document.title = $1;
}
});
obs.observe(table, {'childList': true});)";
EXPECT_TRUE(
ExecJsInWebUI(JsReplace(kObserveEmptySourcesTableScript, kDeleteTitle)));
// Click the button.
EXPECT_TRUE(ExecJsInWebUI("document.getElementById('clear-data').click();"));
EXPECT_EQ(kDeleteTitle, delete_title_watcher.WaitAndGetTitle());
}
IN_PROC_BROWSER_TEST_F(AttributionInternalsWebUiBrowserTest,
WebUISendReports_ReportsRemoved) {
EXPECT_TRUE(NavigateToURL(shell(), GURL(kAttributionInternalsUrl)));
EXPECT_CALL(*manager(), GetPendingReportsForInternalUse)
.WillOnce(InvokeCallback(
{ReportBuilder(
AttributionInfoBuilder(SourceBuilder().BuildStored()).Build())
.SetPriority(7)
.SetReportId(AttributionReport::EventLevelData::Id(5))
.Build()}))
.WillOnce(InvokeCallback(std::vector<AttributionReport>{}))
.WillOnce(InvokeCallback(std::vector<AttributionReport>{}));
EXPECT_CALL(
*manager(),
SendReportsForWebUI(
ElementsAre(VariantWith<AttributionReport::EventLevelData::Id>(
AttributionReport::EventLevelData::Id(5))),
_))
.WillOnce([](const std::vector<AttributionReport::Id>& ids,
base::OnceClosure done) { std::move(done).Run(); });
static constexpr char wait_script[] = R"(
let table = document.querySelector('#reportTable')
.shadowRoot.querySelector('tbody');
const setTitleIfDone = (_, obs) => {
if (table.children.length === 1 &&
table.children[0].children.length >= 7 &&
table.children[0].children[6].innerText === "7") {
if (obs) {
obs.disconnect();
}
document.title = $1;
return true;
}
return false;
};
if (!setTitleIfDone()) {
let obs = new MutationObserver(setTitleIfDone);
obs.observe(table, {'childList': true});
})";
ASSERT_TRUE(ExecJsInWebUI(JsReplace(wait_script, kCompleteTitle)));
// Wait for the table to rendered.
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
ClickRefreshButton();
ASSERT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle());
// Click the send reports button and expect that the report table is emptied.
const std::u16string kSentTitle = u"Sent";
TitleWatcher sent_title_watcher(shell()->web_contents(), kSentTitle);
SetTitleOnReportsTableEmpty(kSentTitle);
ASSERT_TRUE(ExecJsInWebUI(
R"(document.querySelector('#reportTable')
.shadowRoot.querySelector('input[type="checkbox"]').click();)"));
ASSERT_TRUE(
ExecJsInWebUI("document.getElementById('send-reports').click();"));
// The real manager would do this itself, but the test manager requires manual
// triggering.
manager()->NotifyReportsChanged(AttributionReport::ReportType::kEventLevel);
ASSERT_EQ(kSentTitle, sent_title_watcher.WaitAndGetTitle());
}
IN_PROC_BROWSER_TEST_F(AttributionInternalsWebUiBrowserTest,
MojoJsBindingsCorrectlyScoped) {
EXPECT_TRUE(NavigateToURL(shell(), GURL(kAttributionInternalsUrl)));
const std::u16string passed_title = u"passed";
{
TitleWatcher sent_title_watcher(shell()->web_contents(), passed_title);
EXPECT_TRUE(
ExecJsInWebUI("document.title = window.Mojo? 'passed' : 'failed';"));
EXPECT_EQ(passed_title, sent_title_watcher.WaitAndGetTitle());
}
EXPECT_TRUE(NavigateToURL(shell(), GURL("about:blank")));
{
TitleWatcher sent_title_watcher(shell()->web_contents(), passed_title);
EXPECT_TRUE(
ExecJsInWebUI("document.title = window.Mojo? 'failed' : 'passed';"));
EXPECT_EQ(passed_title, sent_title_watcher.WaitAndGetTitle());
}
}
IN_PROC_BROWSER_TEST_F(
AttributionInternalsWebUiBrowserTest,
WebUIShownWithPendingAggregatableReports_ReportsDisplayed) {
EXPECT_TRUE(NavigateToURL(shell(), GURL(kAttributionInternalsUrl)));
const base::Time now = base::Time::Now();
std::vector<AggregatableHistogramContribution> contributions{
AggregatableHistogramContribution(1, 2)};
manager()->NotifyReportSent(
ReportBuilder(
AttributionInfoBuilder(SourceBuilder(now).BuildStored()).Build())
.SetReportTime(now + base::Hours(3))
.SetAggregatableHistogramContributions(contributions)
.BuildAggregatableAttribution(),
/*is_debug_report=*/false,
SendResult(SendResult::Status::kSent, net::OK,
/*http_response_code=*/200));
manager()->NotifyReportSent(
ReportBuilder(
AttributionInfoBuilder(SourceBuilder(now).BuildStored()).Build())
.SetReportTime(now + base::Hours(4))
.SetAggregatableHistogramContributions(contributions)
.BuildAggregatableAttribution(),
/*is_debug_report=*/false, SendResult(SendResult::Status::kDropped));
manager()->NotifyReportSent(
ReportBuilder(
AttributionInfoBuilder(SourceBuilder(now).BuildStored()).Build())
.SetReportTime(now + base::Hours(5))
.SetAggregatableHistogramContributions(contributions)
.BuildAggregatableAttribution(),
/*is_debug_report=*/false,
SendResult(SendResult::Status::kFailedToAssemble));
manager()->NotifyReportSent(
ReportBuilder(
AttributionInfoBuilder(SourceBuilder(now).BuildStored()).Build())
.SetReportTime(now + base::Hours(6))
.SetAggregatableHistogramContributions(contributions)
.BuildAggregatableAttribution(),
/*is_debug_report=*/false,
SendResult(SendResult::Status::kFailure, net::ERR_INVALID_REDIRECT));
manager()->NotifyReportSent(
ReportBuilder(
AttributionInfoBuilder(SourceBuilder(now).BuildStored()).Build())
.SetReportTime(now + base::Hours(10))
.SetAggregatableHistogramContributions(contributions)
.BuildAggregatableAttribution(),
/*is_debug_report=*/true,
SendResult(SendResult::Status::kTransientFailure,
net::ERR_INTERNET_DISCONNECTED));
ON_CALL(*manager(), GetPendingReportsForInternalUse)
.WillByDefault(InvokeCallback(
{ReportBuilder(AttributionInfoBuilder(
SourceBuilder(now)
.SetSourceType(AttributionSourceType::kEvent)
.BuildStored())
.Build())
.SetReportTime(now)
.SetAggregatableHistogramContributions(contributions)
.BuildAggregatableAttribution()}));
{
static constexpr char wait_script[] = R"(
let table = document.querySelector('#aggregatableReportTable')
.shadowRoot.querySelector('tbody');
let obs = new MutationObserver((_, obs) => {
if (table.children.length === 6 &&
table.children[0].children[3].innerText ===
"https://report.test/.well-known/attribution-reporting/report-aggregate-attribution" &&
table.children[0].children[2].innerText === "Pending" &&
table.children[0].children[6].innerText === '[ { "key": "0x1", "value": 2 }]' &&
table.children[1].children[2].innerText === "Sent: HTTP 200" &&
table.children[2].children[2].innerText === "Prohibited by browser policy" &&
table.children[3].children[2].innerText === "Dropped due to assembly failure" &&
table.children[4].children[2].innerText === "Network error: ERR_INVALID_REDIRECT" &&
table.children[5].children[2].innerText === "Network error: ERR_INTERNET_DISCONNECTED" &&
table.children[5].children[3].innerText ===
"https://report.test/.well-known/attribution-reporting/debug/report-aggregate-attribution") {
obs.disconnect();
document.title = $1;
}
});
obs.observe(table, {'childList': true});)";
EXPECT_TRUE(ExecJsInWebUI(JsReplace(wait_script, kCompleteTitle)));
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
ClickRefreshButton();
EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle());
}
}
IN_PROC_BROWSER_TEST_F(AttributionInternalsWebUiBrowserTest,
TriggersDisplayed) {
EXPECT_TRUE(NavigateToURL(shell(), GURL(kAttributionInternalsUrl)));
const base::Time now = base::Time::Now();
const AttributionTrigger trigger(
url::Origin::Create(GURL("https://d.test")),
url::Origin::Create(GURL("https://r.test")),
/*filters=*/AttributionFilterData::CreateForTesting({{"a", {"b"}}}),
/*not_filters=*/AttributionFilterData::CreateForTesting({{"g", {"h"}}}),
/*debug_key=*/1,
{
AttributionTrigger::EventTriggerData(
/*data=*/2,
/*priority=*/3,
/*dedup_key=*/absl::nullopt,
/*filters=*/
AttributionFilterData::CreateForTesting({{"c", {"d"}}}),
/*not_filters=*/AttributionFilterData()),
AttributionTrigger::EventTriggerData(
/*data=*/4,
/*priority=*/5,
/*dedup_key=*/6,
/*filters=*/AttributionFilterData(),
/*not_filters=*/
AttributionFilterData::CreateForTesting({{"e", {"f"}}})),
},
{AttributionAggregatableTriggerData::CreateForTesting(
/*key_piece=*/345,
/*source_keys=*/{"a"},
/*filters=*/
AttributionFilterData::CreateForTesting({{"c", {"d"}}}),
/*not_filters=*/AttributionFilterData()),
AttributionAggregatableTriggerData::CreateForTesting(
/*key_piece=*/678,
/*source_keys=*/{"b"},
/*filters=*/AttributionFilterData(),
/*not_filters=*/
AttributionFilterData::CreateForTesting({{"e", {"f"}}}))},
/*aggregatable_values=*/
AttributionAggregatableValues::CreateForTesting(
{{"a", 123}, {"b", 456}}));
static constexpr char kWantEventTriggerJSON[] =
R"json([ { "data": "2", "priority": "3", "filters": { "c": [ "d" ] } }, { "data": "4", "priority": "5", "deduplication_key": "6", "not_filters": { "e": [ "f" ] } }])json";
static constexpr char kWantAggregatableTriggerJSON[] =
R"json([ { "key_piece": "0x159", "source_keys": [ "a" ], "filters": { "c": [ "d" ] } }, { "key_piece": "0x2a6", "source_keys": [ "b" ], "not_filters": { "e": [ "f" ] } }])json";
static constexpr char wait_script[] = R"(
let table = document.querySelector('#triggerTable')
.shadowRoot.querySelector('tbody');
let obs = new MutationObserver((_, obs) => {
if (table.children.length === 1 &&
table.children[0].children[1].innerText === "Success: Report stored" &&
table.children[0].children[2].innerText === "Success: Report stored" &&
table.children[0].children[3].innerText === "https://d.test" &&
table.children[0].children[4].innerText === "https://r.test" &&
table.children[0].children[5].innerText === "1" &&
table.children[0].children[6].innerText === '{ "a": [ "b" ]}' &&
table.children[0].children[7].innerText === '{ "g": [ "h" ]}' &&
table.children[0].children[8].innerText === $2 &&
table.children[0].children[9].innerText === $3 &&
table.children[0].children[10].innerText === '{ "a": 123, "b": 456}') {
obs.disconnect();
document.title = $1;
}
});
obs.observe(table, {'childList': true});)";
EXPECT_TRUE(ExecJsInWebUI(JsReplace(wait_script, kCompleteTitle,
kWantEventTriggerJSON,
kWantAggregatableTriggerJSON)));
auto notify_trigger_handled =
[&](AttributionTrigger::EventLevelResult event_status,
AttributionTrigger::AggregatableResult aggregatable_status) {
static int offset_hours = 0;
manager()->NotifyTriggerHandled(
trigger,
CreateReportResult(
/*trigger_time=*/now + base::Hours(++offset_hours),
event_status, aggregatable_status,
/*replaced_event_level_report=*/absl::nullopt,
/*new_event_level_report=*/IrreleventEventLevelReport(),
/*new_aggregatable_report=*/IrreleventAggregatableReport()));
};
notify_trigger_handled(AttributionTrigger::EventLevelResult::kSuccess,
AttributionTrigger::AggregatableResult::kSuccess);
// TODO(apaseltiner): Add tests for other statuses.
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
ClickRefreshButton();
EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle());
}
IN_PROC_BROWSER_TEST_F(AttributionInternalsWebUiBrowserTest,
WebUISendAggregatableReports_ReportsRemoved) {
EXPECT_TRUE(NavigateToURL(shell(), GURL(kAttributionInternalsUrl)));
EXPECT_CALL(*manager(), GetPendingReportsForInternalUse)
.WillOnce(InvokeCallback(std::vector<AttributionReport>{}))
.WillOnce(InvokeCallback(
{ReportBuilder(
AttributionInfoBuilder(SourceBuilder().BuildStored()).Build())
.SetReportId(
AttributionReport::AggregatableAttributionData::Id(5))
.SetAggregatableHistogramContributions(
{AggregatableHistogramContribution(1, 2)})
.BuildAggregatableAttribution()}))
.WillOnce(InvokeCallback(std::vector<AttributionReport>{}));
EXPECT_CALL(
*manager(),
SendReportsForWebUI(
ElementsAre(
VariantWith<AttributionReport::AggregatableAttributionData::Id>(
AttributionReport::AggregatableAttributionData::Id(5))),
_))
.WillOnce([](const std::vector<AttributionReport::Id>& ids,
base::OnceClosure done) { std::move(done).Run(); });
static constexpr char wait_script[] = R"(
let table = document.querySelector('#aggregatableReportTable')
.shadowRoot.querySelector('tbody');
let obs = new MutationObserver((_, obs) => {
if (table.children.length === 1) {
obs.disconnect();
document.title = $1;
}
});
obs.observe(table, {'childList': true});)";
EXPECT_TRUE(ExecJsInWebUI(JsReplace(wait_script, kCompleteTitle)));
// Wait for the table to rendered.
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
ClickRefreshButton();
EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle());
// Click the send reports button and expect that the report table is emptied.
const std::u16string kSentTitle = u"Sent";
TitleWatcher sent_title_watcher(shell()->web_contents(), kSentTitle);
static constexpr char kObserveEmptyReportsTableScript[] = R"(
let table = document.querySelector('#aggregatableReportTable')
.shadowRoot.querySelector('tbody');
let obs = new MutationObserver((_, obs) => {
if (table.children.length === 1 &&
table.children[0].children[0].innerText === "No sent or pending reports.") {
obs.disconnect();
document.title = $1;
}
});
obs.observe(table, {'childList': true});)";
EXPECT_TRUE(
ExecJsInWebUI(JsReplace(kObserveEmptyReportsTableScript, kSentTitle)));
EXPECT_TRUE(ExecJsInWebUI(
R"(document.querySelector('#aggregatableReportTable')
.shadowRoot.querySelectorAll('input[type="checkbox"]')[1].click();)"));
EXPECT_TRUE(ExecJsInWebUI(
"document.getElementById('send-aggregatable-reports').click();"));
// The real manager would do this itself, but the test manager requires manual
// triggering.
manager()->NotifyReportsChanged(
AttributionReport::ReportType::kAggregatableAttribution);
EXPECT_EQ(kSentTitle, sent_title_watcher.WaitAndGetTitle());
}
IN_PROC_BROWSER_TEST_F(AttributionInternalsWebUiBrowserTest,
ToggleDebugReports) {
EXPECT_TRUE(NavigateToURL(shell(), GURL(kAttributionInternalsUrl)));
const base::Time now = base::Time::Now();
manager()->NotifyReportSent(
ReportBuilder(
AttributionInfoBuilder(SourceBuilder(now).BuildStored()).Build())
.SetReportTime(now)
.SetPriority(1)
.Build(),
/*is_debug_report=*/true,
SendResult(SendResult::Status::kSent, net::OK,
/*http_response_code=*/200));
ON_CALL(*manager(), GetPendingReportsForInternalUse)
.WillByDefault(InvokeCallback(
{ReportBuilder(
AttributionInfoBuilder(SourceBuilder(now).BuildStored()).Build())
.SetReportTime(now + base::Hours(1))
.SetPriority(2)
.Build()}));
// By default, debug reports are shown.
{
static constexpr char wait_script[] = R"(
let table = document.querySelector('#reportTable')
.shadowRoot.querySelector('tbody');
let label = document.querySelector('#show-debug-event-reports span');
let obs = new MutationObserver((_, obs) => {
if (table.children.length === 2 &&
table.children[0].children[6].innerText === "1" &&
table.children[1].children[6].innerText === "2" &&
label.innerText === '') {
obs.disconnect();
document.title = $1;
}
});
obs.observe(table, {'childList': true});
obs.observe(label, {'characterData': true});)";
EXPECT_TRUE(ExecJsInWebUI(JsReplace(wait_script, kCompleteTitle)));
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
ClickRefreshButton();
EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle());
}
// Toggle checkbox.
EXPECT_TRUE(ExecJsInWebUI(R"(
document.querySelector('#show-debug-event-reports input').click();)"));
manager()->NotifyReportSent(
ReportBuilder(
AttributionInfoBuilder(SourceBuilder(now).BuildStored()).Build())
.SetReportTime(now + base::Hours(2))
.SetPriority(3)
.Build(),
/*is_debug_report=*/true,
SendResult(SendResult::Status::kSent, net::OK,
/*http_response_code=*/200));
// The debug reports, including the newly received one, should be hidden and
// the label should indicate the number.
{
static constexpr char wait_script[] = R"(
let table = document.querySelector('#reportTable')
.shadowRoot.querySelector('tbody');
let label = document.querySelector('#show-debug-event-reports span');
let obs = new MutationObserver((_, obs) => {
if (table.children.length === 1 &&
table.children[0].children[6].innerText === "2" &&
label.innerText === ' (2 hidden)') {
obs.disconnect();
document.title = $1;
}
});
obs.observe(table, {'childList': true});
obs.observe(label, {'characterData': true});)";
EXPECT_TRUE(ExecJsInWebUI(JsReplace(wait_script, kCompleteTitle2)));
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle2);
ClickRefreshButton();
EXPECT_EQ(kCompleteTitle2, title_watcher.WaitAndGetTitle());
}
// Toggle checkbox.
EXPECT_TRUE(ExecJsInWebUI(R"(
document.querySelector('#show-debug-event-reports input').click();)"));
// The debug reports should be visible again and the hidden label should be
// cleared.
{
static constexpr char wait_script[] = R"(
let table = document.querySelector('#reportTable').shadowRoot
.querySelector('tbody');
let label = document.querySelector('#show-debug-event-reports span');
let obs = new MutationObserver((_, obs) => {
if (table.children.length === 3 &&
table.children[0].children[6].innerText === "1" &&
table.children[1].children[6].innerText === "2" &&
table.children[2].children[6].innerText === "3" &&
label.innerText === '') {
obs.disconnect();
document.title = $1;
}
});
obs.observe(table, {'childList': true});
obs.observe(label, {'characterData': true});)";
EXPECT_TRUE(ExecJsInWebUI(JsReplace(wait_script, kCompleteTitle3)));
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle3);
ClickRefreshButton();
EXPECT_EQ(kCompleteTitle3, title_watcher.WaitAndGetTitle());
}
}
} // namespace content