blob: 08e00ac51ad32d72d65ec888b30a2eed7e39e2e4 [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// 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/command_line.h"
#include "base/functional/callback.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/gmock_callback_support.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "build/buildflag.h"
#include "components/aggregation_service/aggregation_service.mojom.h"
#include "components/attribution_reporting/aggregatable_dedup_key.h"
#include "components/attribution_reporting/aggregatable_trigger_data.h"
#include "components/attribution_reporting/aggregatable_values.h"
#include "components/attribution_reporting/aggregation_keys.h"
#include "components/attribution_reporting/event_trigger_data.h"
#include "components/attribution_reporting/filters.h"
#include "components/attribution_reporting/source_registration_error.mojom.h"
#include "components/attribution_reporting/source_type.mojom.h"
#include "components/attribution_reporting/suitable_origin.h"
#include "components/attribution_reporting/trigger_registration.h"
#include "content/browser/attribution_reporting/attribution_debug_report.h"
#include "content/browser/attribution_reporting/attribution_manager.h"
#include "content/browser/attribution_reporting/attribution_report.h"
#include "content/browser/attribution_reporting/attribution_test_utils.h"
#include "content/browser/attribution_reporting/attribution_trigger.h"
#include "content/browser/attribution_reporting/create_report_result.h"
#include "content/browser/attribution_reporting/send_result.h"
#include "content/browser/attribution_reporting/storable_source.h"
#include "content/browser/attribution_reporting/store_source_result.h"
#include "content/browser/attribution_reporting/stored_source.h"
#include "content/browser/attribution_reporting/test/mock_attribution_manager.h"
#include "content/browser/attribution_reporting/test/mock_content_browser_client.h"
#include "content/browser/storage_partition_impl.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browsing_data_filter_builder.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_content_browser_client.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 "net/base/schemeful_site.h"
#include "services/network/public/cpp/trigger_attestation.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "url/origin.h"
#if BUILDFLAG(IS_ANDROID)
#include "components/attribution_reporting/os_support.mojom.h"
#include "content/browser/attribution_reporting/attribution_input_event.h"
#include "content/browser/attribution_reporting/attribution_os_level_manager_android.h"
#include "content/browser/attribution_reporting/os_registration.h"
#endif
namespace content {
namespace {
using ::attribution_reporting::FilterPair;
using ::attribution_reporting::SuitableOrigin;
using ::attribution_reporting::mojom::SourceRegistrationError;
using ::attribution_reporting::mojom::SourceType;
using ::base::test::RunOnceCallback;
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";
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>();
ON_CALL(*manager, GetActiveSourcesForWebUI)
.WillByDefault(RunOnceCallback<0>(std::vector<StoredSource>{}));
ON_CALL(*manager, GetPendingReportsForInternalUse)
.WillByDefault(RunOnceCallback<1>(std::vector<AttributionReport>{}));
static_cast<StoragePartitionImpl*>(shell()
->web_contents()
->GetBrowserContext()
->GetDefaultStoragePartition())
->OverrideAttributionManagerForTesting(std::move(manager));
}
void ClickRefreshButton() {
ASSERT_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"(
const table = document.querySelector('#reportTable')
.shadowRoot.querySelector('tbody');
const 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, subtree: true, characterData: true});
)";
ASSERT_TRUE(
ExecJsInWebUI(JsReplace(kObserveEmptyReportsTableScript, title)));
}
MockAttributionManager* manager() {
AttributionManager* manager =
static_cast<StoragePartitionImpl*>(shell()
->web_contents()
->GetBrowserContext()
->GetDefaultStoragePartition())
->GetAttributionManager();
return static_cast<MockAttributionManager*>(manager);
}
};
IN_PROC_BROWSER_TEST_F(AttributionInternalsWebUiBrowserTest,
NavigationUrl_ResolvedToWebUI) {
ASSERT_TRUE(NavigateToURL(shell(), GURL(kAttributionInternalsUrl)));
static constexpr char kScript[] = R"(
document.body.innerHTML.search('Attribution Reporting API Internals') >= 0;
)";
// Execute script to ensure the page has loaded correctly, executing similarly
// to ExecJsInWebUI().
EXPECT_EQ(true,
EvalJs(shell()->web_contents()->GetPrimaryMainFrame(), kScript,
EXECUTE_SCRIPT_DEFAULT_OPTIONS, /*world_id=*/1));
}
IN_PROC_BROWSER_TEST_F(AttributionInternalsWebUiBrowserTest,
WebUIShownWithManager_MeasurementConsideredEnabled) {
ASSERT_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 kScript[] = R"(
const status = document.getElementById('feature-status-content');
const obs = new MutationObserver((_, obs) => {
if (status.innerText.trim() === 'enabled') {
obs.disconnect();
document.title = $1;
}
});
obs.observe(status, {childList: true, characterData: true});
)";
ASSERT_TRUE(ExecJsInWebUI(JsReplace(kScript, kCompleteTitle)));
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
ClickRefreshButton();
EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle());
}
IN_PROC_BROWSER_TEST_F(AttributionInternalsWebUiBrowserTest,
DisabledByEmbedder_MeasurementConsideredDisabled) {
MockAttributionReportingContentBrowserClientBase<
ContentBrowserTestContentBrowserClient>
browser_client;
EXPECT_CALL(browser_client,
IsAttributionReportingOperationAllowed(
_, ContentBrowserClient::AttributionReportingOperation::kAny,
_, IsNull(), IsNull(), IsNull()))
.WillRepeatedly(Return(false));
ASSERT_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 kScript[] = R"(
const status = document.getElementById('feature-status-content');
const obs = new MutationObserver((_, obs) => {
if (status.innerText.trim() === 'disabled') {
obs.disconnect();
document.title = $1;
}
});
obs.observe(status, {childList: true, characterData: true});
)";
ASSERT_TRUE(ExecJsInWebUI(JsReplace(kScript, kCompleteTitle)));
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
ClickRefreshButton();
EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle());
}
IN_PROC_BROWSER_TEST_F(
AttributionInternalsWebUiBrowserTest,
WebUIShownWithNoActiveImpression_NoImpressionsDisplayed) {
ASSERT_TRUE(NavigateToURL(shell(), GURL(kAttributionInternalsUrl)));
static constexpr char kScript[] = R"(
const table = document.querySelector('#sourceTable')
.shadowRoot.querySelector('tbody');
const 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, subtree: true, characterData: true});
)";
ASSERT_TRUE(ExecJsInWebUI(JsReplace(kScript, kCompleteTitle)));
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
ClickRefreshButton();
EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle());
}
IN_PROC_BROWSER_TEST_F(AttributionInternalsWebUiBrowserTest,
WebUIShownWithActiveImpression_ImpressionsDisplayed) {
ASSERT_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(RunOnceCallback<0>(std::vector<StoredSource>{
SourceBuilder(now)
.SetSourceEventId(std::numeric_limits<uint64_t>::max())
.SetAttributionLogic(StoredSource::AttributionLogic::kNever)
.SetDebugKey(19)
.SetDestinationSites({
net::SchemefulSite::Deserialize("https://a.test"),
net::SchemefulSite::Deserialize("https://b.test"),
})
.BuildStored(),
SourceBuilder(now + base::Hours(1))
.SetSourceType(SourceType::kEvent)
.SetPriority(std::numeric_limits<int64_t>::max())
.SetDedupKeys({13, 17})
.SetAggregatableBudgetConsumed(1300)
.SetFilterData(*attribution_reporting::FilterData::Create(
{{"a", {"b", "c"}}}))
.SetAggregationKeys(
*attribution_reporting::AggregationKeys::FromKeys({{"a", 1}}))
.SetAggregatableDedupKeys({14, 18})
.BuildStored(),
SourceBuilder(now + base::Hours(2))
.SetActiveState(
StoredSource::ActiveState::kReachedEventLevelAttributionLimit)
.BuildStored(),
SourceBuilder(now + base::Hours(8))
.SetAttributionLogic(StoredSource::AttributionLogic::kFalsely)
.BuildStored()}));
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,
/*cleared_debug_key=*/987);
manager()->NotifySourceHandled(
SourceBuilder(now + base::Hours(6)).Build(),
StorableSource::Result::kInsufficientUniqueDestinationCapacity);
manager()->NotifySourceHandled(
SourceBuilder(now + base::Hours(7))
.SetSourceType(SourceType::kEvent)
.Build(),
StorableSource::Result::kExcessiveReportingOrigins);
static constexpr char kScript[] = R"(
const table = document.querySelector('#sourceTable')
.shadowRoot.querySelector('tbody');
const regTable = document.querySelector('#sourceRegistrationTable')
.shadowRoot.querySelector('tbody');
const obs = new MutationObserver((_, obs) => {
if (table.children.length === 4 &&
regTable.children.length === 5 &&
table.children[0].children[3]?.children[0]?.children.length === 2 &&
table.children[0].children[3]?.children[0]?.children[0]?.innerText === 'https://a.test' &&
table.children[0].children[3]?.children[0]?.children[1]?.innerText === 'https://b.test' &&
table.children[1].children[3]?.innerText === 'https://conversion.test' &&
table.children[0].children[0]?.innerText === $1 &&
table.children[0].children[9]?.innerText === 'Navigation' &&
table.children[1].children[9]?.innerText === 'Event' &&
table.children[0].children[10]?.innerText === '0' &&
table.children[1].children[10]?.innerText === $2 &&
table.children[0].children[11]?.innerText === '{}' &&
table.children[1].children[11]?.innerText === '{\n "a": [\n "b",\n "c"\n ]\n}' &&
table.children[0].children[12]?.innerText === '{}' &&
table.children[1].children[12]?.innerText === '{\n "a": "0x1"\n}' &&
table.children[0].children[13]?.innerText === '0 / 65536' &&
table.children[1].children[13]?.innerText === '1300 / 65536' &&
table.children[0].children[14]?.innerText === '19' &&
table.children[1].children[14]?.innerText === '' &&
table.children[0].children[15]?.innerText === '' &&
table.children[1].children[15]?.children[0]?.children[0]?.innerText === '13' &&
table.children[1].children[15]?.children[0]?.children[1]?.innerText === '17' &&
table.children[0].children[16]?.innerText === '' &&
table.children[1].children[16]?.children[0]?.children[0]?.innerText === '14' &&
table.children[1].children[16]?.children[0]?.children[1]?.innerText === '18' &&
table.children[0].children[1]?.innerText === 'Unattributable: noised with no reports' &&
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: noised with fake reports' &&
regTable.children[0].children[4]?.innerText === '' &&
regTable.children[0].children[6]?.innerText === 'Success' &&
regTable.children[1].children[6]?.innerText === 'Rejected: internal error' &&
regTable.children[2].children[6]?.innerText === 'Rejected: insufficient source capacity' &&
regTable.children[2].children[4]?.innerText === '987' &&
regTable.children[3].children[5]?.innerText === 'Navigation' &&
regTable.children[3].children[6]?.innerText === 'Rejected: insufficient unique destination capacity' &&
regTable.children[4].children[5]?.innerText === 'Event' &&
regTable.children[4].children[6]?.innerText === 'Rejected: excessive reporting origins') {
obs.disconnect();
document.title = $3;
}
});
obs.observe(table, {childList: true, subtree: true, characterData: true});
)";
ASSERT_TRUE(ExecJsInWebUI(
JsReplace(kScript, kMaxUint64String, kMaxInt64String, kCompleteTitle)));
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
ClickRefreshButton();
EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle());
}
IN_PROC_BROWSER_TEST_F(AttributionInternalsWebUiBrowserTest,
FailedSourceRegistrationLogShown) {
ASSERT_TRUE(NavigateToURL(shell(), GURL(kAttributionInternalsUrl)));
static constexpr char kScript[] = R"(
const table = document.querySelector('#sourceRegistrationTable')
.shadowRoot.querySelector('tbody');
const obs = new MutationObserver((_, obs) => {
if (table.children.length === 1 &&
table.children[0].children[1]?.innerText === 'https://b.test' &&
table.children[0].children[2]?.innerText === 'https://a.test' &&
table.children[0].children[3]?.innerText === '!' &&
table.children[0].children[4]?.innerText === '' &&
table.children[0].children[5]?.innerText === 'Event' &&
table.children[0].children[6]?.innerText === 'Rejected: invalid JSON: invalid syntax') {
obs.disconnect();
document.title = $1;
}
});
obs.observe(table, {childList: true, subtree: true, characterData: true});
)";
ASSERT_TRUE(ExecJsInWebUI(JsReplace(kScript, kCompleteTitle)));
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
manager()->NotifySourceRegistrationFailure(
"!", *SuitableOrigin::Deserialize("https://b.test"),
*SuitableOrigin::Deserialize("https://a.test"), SourceType::kEvent,
SourceRegistrationError::kInvalidJson);
EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle());
}
#if BUILDFLAG(IS_ANDROID)
IN_PROC_BROWSER_TEST_F(AttributionInternalsWebUiBrowserTest,
OsRegistrationsShown) {
ASSERT_TRUE(NavigateToURL(shell(), GURL(kAttributionInternalsUrl)));
static constexpr char kScript[] = R"(
const table = document.querySelector('#osRegistrationTable')
.shadowRoot.querySelector('tbody');
const obs = new MutationObserver((_, obs) => {
if (table.children.length === 1 &&
table.children[0].children[1]?.innerText === 'OS Source' &&
table.children[0].children[2]?.innerText === 'https://a.test/' &&
table.children[0].children[3]?.innerText === 'https://b.test' &&
table.children[0].children[4]?.innerText === 'false') {
obs.disconnect();
document.title = $1;
}
});
obs.observe(table, {childList: true, subtree: true, characterData: true});
)";
ASSERT_TRUE(ExecJsInWebUI(JsReplace(kScript, kCompleteTitle)));
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
manager()->NotifyOsRegistration(
OsRegistration(GURL("https://a.test"),
url::Origin::Create(GURL("https://b.test")),
AttributionInputEvent()),
/*is_debug_key_allowed=*/false);
EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle());
}
#endif // BUILDFLAG(IS_ANDROID)
IN_PROC_BROWSER_TEST_F(AttributionInternalsWebUiBrowserTest,
WebUIShownWithNoReports_NoReportsDisplayed) {
ASSERT_TRUE(NavigateToURL(shell(), GURL(kAttributionInternalsUrl)));
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
SetTitleOnReportsTableEmpty(kCompleteTitle);
ClickRefreshButton();
ASSERT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle());
}
IN_PROC_BROWSER_TEST_F(AttributionInternalsWebUiBrowserTest,
WebUIShownWithManager_DebugModeDisabled) {
ASSERT_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 kScript[] = R"(
const status = document.getElementById('debug-mode-content');
const obs = new MutationObserver((_, obs) => {
if (status.innerText.trim() === '') {
obs.disconnect();
document.title = $1;
}
});
obs.observe(status, {childList: true, subtree: true, characterData: true});
)";
ASSERT_TRUE(ExecJsInWebUI(JsReplace(kScript, 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::kAttributionReportingDebugMode);
ASSERT_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 kScript[] = R"(
const status = document.getElementById('debug-mode-content');
const obs = new MutationObserver((_, obs) => {
if (status.innerText.trim() !== '') {
obs.disconnect();
document.title = $1;
}
});
obs.observe(status, {childList: true, subtree: true, characterData: true});
)";
ASSERT_TRUE(ExecJsInWebUI(JsReplace(kScript, kCompleteTitle)));
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
ClickRefreshButton();
EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle());
}
IN_PROC_BROWSER_TEST_F(AttributionInternalsWebUiBrowserTest,
WebUIShownWithManager_OsSupportDisabled) {
ASSERT_TRUE(NavigateToURL(shell(), GURL(kAttributionInternalsUrl)));
static constexpr char kScript[] = R"(
const status = document.getElementById('os-support');
const setTitleIfDone = (_, obs) => {
if (status.innerText === 'disabled') {
if (obs) {
obs.disconnect();
}
document.title = $1;
return true;
}
return false;
};
if (!setTitleIfDone()) {
const obs = new MutationObserver(setTitleIfDone);
obs.observe(status, {childList: true, subtree: true, characterData: true});
}
)";
ASSERT_TRUE(ExecJsInWebUI(JsReplace(kScript, kCompleteTitle)));
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
ClickRefreshButton();
EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle());
}
#if BUILDFLAG(IS_ANDROID)
IN_PROC_BROWSER_TEST_F(AttributionInternalsWebUiBrowserTest,
WebUIShownWithManager_OsSupportEnabled) {
ASSERT_TRUE(NavigateToURL(shell(), GURL(kAttributionInternalsUrl)));
static constexpr char kScript[] = R"(
const status = document.getElementById('os-support');
const setTitleIfDone = (_, obs) => {
if (status.innerText === 'enabled') {
if (obs) {
obs.disconnect();
}
document.title = $1;
return true;
}
return false;
};
if (!setTitleIfDone()) {
const obs = new MutationObserver(setTitleIfDone);
obs.observe(status, {childList: true, subtree: true, characterData: true});
}
)";
ASSERT_TRUE(ExecJsInWebUI(JsReplace(kScript, kCompleteTitle)));
AttributionOsLevelManagerAndroid::ScopedOsSupportForTesting
scoped_os_support_setting(
attribution_reporting::mojom::OsSupport::kEnabled);
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
ClickRefreshButton();
EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle());
}
#endif // BUILDFLAG(IS_ANDROID)
IN_PROC_BROWSER_TEST_F(AttributionInternalsWebUiBrowserTest,
WebUIShownWithPendingReports_ReportsDisplayed) {
ASSERT_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(RunOnceCallback<1>(std::vector<AttributionReport>{
ReportBuilder(AttributionInfoBuilder(
SourceBuilder(now)
.SetSourceType(SourceType::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(),
/*new_aggregatable_report=*/absl::nullopt,
/*source=*/SourceBuilder().BuildStored()));
{
static constexpr char kScript[] = R"(
const table = document.querySelector('#reportTable')
.shadowRoot.querySelector('tbody');
const 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[2].classList.contains('send-error') &&
table.children[3].children[2]?.innerText === 'Prohibited by browser policy' &&
!table.children[3].classList.contains('send-error') &&
table.children[4].children[2]?.innerText === 'Network error: ERR_METHOD_NOT_SUPPORTED' &&
table.children[4].classList.contains('send-error') &&
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,
subtree: true,
characterData: true,
attributes: true,
});
)";
ASSERT_TRUE(ExecJsInWebUI(JsReplace(kScript, kCompleteTitle)));
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
ClickRefreshButton();
ASSERT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle());
}
{
static constexpr char kScript[] = R"(
const table = document.querySelector('#reportTable')
.shadowRoot.querySelector('tbody');
const 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, subtree: true, characterData: true});
)";
ASSERT_TRUE(ExecJsInWebUI(JsReplace(kScript, kCompleteTitle2)));
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle2);
// Sort by priority ascending.
ASSERT_TRUE(ExecJsInWebUI(R"(
document.querySelector('#reportTable')
.shadowRoot.querySelectorAll('th')[6].click();
)"));
ASSERT_EQ(kCompleteTitle2, title_watcher.WaitAndGetTitle());
}
{
static constexpr char kScript[] = R"(
const table = document.querySelector('#reportTable')
.shadowRoot.querySelector('tbody');
const 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, subtree: true, characterData: true});
)";
ASSERT_TRUE(ExecJsInWebUI(JsReplace(kScript, kCompleteTitle3)));
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle3);
// Sort by priority descending.
ASSERT_TRUE(ExecJsInWebUI(R"(
document.querySelector('#reportTable')
.shadowRoot.querySelectorAll('th')[6].click();
)"));
ASSERT_EQ(kCompleteTitle3, title_watcher.WaitAndGetTitle());
}
}
IN_PROC_BROWSER_TEST_F(AttributionInternalsWebUiBrowserTest,
WebUIWithPendingReportsClearStorage_ReportsRemoved) {
ASSERT_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();
std::vector<AttributionReport> stored_reports;
stored_reports.push_back(report);
EXPECT_CALL(*manager(), GetPendingReportsForInternalUse)
.WillRepeatedly(
[&](int limit,
base::OnceCallback<void(std::vector<AttributionReport>)>
callback) { std::move(callback).Run(stored_reports); });
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,
BrowsingDataFilterBuilder* filter_builder,
bool delete_rate_limit_data, base::OnceClosure done) {
stored_reports.clear();
std::move(done).Run();
});
// Verify both rows get rendered.
static constexpr char kScript[] = R"(
const table = document.querySelector('#reportTable')
.shadowRoot.querySelector('tbody');
const setTitleIfDone = (_, obs) => {
if (table.children.length === 2 &&
table.children[0].children[6]?.innerText === '7' &&
table.children[1].children[2]?.innerText === 'Sent: HTTP 200') {
if (obs) {
obs.disconnect();
}
document.title = $1;
return true;
}
return false;
};
if (!setTitleIfDone()) {
const obs = new MutationObserver(setTitleIfDone);
obs.observe(table, {childList: true, subtree: true, characterData: true});
}
)";
ASSERT_TRUE(ExecJsInWebUI(JsReplace(kScript, kCompleteTitle)));
// Wait for the table to rendered.
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
ClickRefreshButton();
ASSERT_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.
ASSERT_TRUE(ExecJsInWebUI("document.getElementById('clear-data').click();"));
ASSERT_EQ(kDeleteTitle, delete_title_watcher.WaitAndGetTitle());
}
IN_PROC_BROWSER_TEST_F(AttributionInternalsWebUiBrowserTest,
ClearButton_ClearsSourceTable) {
ASSERT_TRUE(NavigateToURL(shell(), GURL(kAttributionInternalsUrl)));
base::Time now = base::Time::Now();
ON_CALL(*manager(), GetActiveSourcesForWebUI)
.WillByDefault(RunOnceCallback<0>(std::vector<StoredSource>{
SourceBuilder(now).SetSourceEventId(5).BuildStored()}));
manager()->NotifySourceHandled(
SourceBuilder(now + base::Hours(2)).SetSourceEventId(6).Build(),
StorableSource::Result::kInternalError);
EXPECT_CALL(*manager(),
ClearData(base::Time::Min(), base::Time::Max(), _, _, true, _))
.WillOnce([](base::Time delete_begin, base::Time delete_end,
StoragePartition::StorageKeyMatcherFunction filter,
BrowsingDataFilterBuilder* filter_builder,
bool delete_rate_limit_data,
base::OnceClosure done) { std::move(done).Run(); });
// Verify both rows get rendered.
static constexpr char kScript[] = R"(
const table = document.querySelector('#sourceTable')
.shadowRoot.querySelector('tbody');
const regTable = document.querySelector('#sourceRegistrationTable')
.shadowRoot.querySelector('tbody');
const obs = new MutationObserver((_, obs) => {
if (table.children.length === 1 &&
regTable.children.length === 1 &&
table.children[0].children[0]?.innerText === '5' &&
regTable.children[0].children[6]?.innerText === 'Rejected: internal error') {
obs.disconnect();
document.title = $1;
}
});
obs.observe(table, {childList: true, subtree: true, characterData: true});
obs.observe(regTable, {childList: true, subtree: true, characterData: true});
)";
ASSERT_TRUE(ExecJsInWebUI(JsReplace(kScript, kCompleteTitle)));
// Wait for the table to rendered.
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
ClickRefreshButton();
ASSERT_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"(
const table = document.querySelector('#sourceTable')
.shadowRoot.querySelector('tbody');
const regTable = document.querySelector('#sourceRegistrationTable')
.shadowRoot.querySelector('tbody');
const obs = new MutationObserver((_, obs) => {
if (table.children.length === 1 &&
regTable.children.length === 1 &&
table.children[0].children[0]?.innerText === 'No sources.' &&
regTable.children[0].children[0]?.innerText === 'No registrations.') {
obs.disconnect();
document.title = $1;
}
});
obs.observe(table, {childList: true, subtree: true, characterData: true});
obs.observe(regTable, {childList: true, subtree: true, characterData: true});
)";
ASSERT_TRUE(
ExecJsInWebUI(JsReplace(kObserveEmptySourcesTableScript, kDeleteTitle)));
// Click the button.
ASSERT_TRUE(ExecJsInWebUI("document.getElementById('clear-data').click();"));
EXPECT_EQ(kDeleteTitle, delete_title_watcher.WaitAndGetTitle());
}
IN_PROC_BROWSER_TEST_F(AttributionInternalsWebUiBrowserTest,
WebUISendReports_ReportsRemoved) {
ASSERT_TRUE(NavigateToURL(shell(), GURL(kAttributionInternalsUrl)));
EXPECT_CALL(*manager(), GetPendingReportsForInternalUse)
.WillOnce(RunOnceCallback<1>(std::vector<AttributionReport>{
ReportBuilder(
AttributionInfoBuilder(SourceBuilder().BuildStored()).Build())
.SetPriority(7)
.SetReportId(AttributionReport::EventLevelData::Id(5))
.Build()}))
.WillOnce(RunOnceCallback<1>(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 kScript[] = R"(
const table = document.querySelector('#reportTable')
.shadowRoot.querySelector('tbody');
const setTitleIfDone = (_, obs) => {
if (table.children.length === 1 &&
table.children[0].children[6]?.innerText === '7') {
if (obs) {
obs.disconnect();
}
document.title = $1;
return true;
}
return false;
};
if (!setTitleIfDone()) {
const obs = new MutationObserver(setTitleIfDone);
obs.observe(table, {childList: true, subtree: true, characterData: true});
}
)";
ASSERT_TRUE(ExecJsInWebUI(JsReplace(kScript, 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();
ASSERT_EQ(kSentTitle, sent_title_watcher.WaitAndGetTitle());
}
IN_PROC_BROWSER_TEST_F(AttributionInternalsWebUiBrowserTest,
MojoJsBindingsCorrectlyScoped) {
ASSERT_TRUE(NavigateToURL(shell(), GURL(kAttributionInternalsUrl)));
const std::u16string passed_title = u"passed";
{
TitleWatcher sent_title_watcher(shell()->web_contents(), passed_title);
ASSERT_TRUE(
ExecJsInWebUI("document.title = window.Mojo? 'passed' : 'failed';"));
ASSERT_EQ(passed_title, sent_title_watcher.WaitAndGetTitle());
}
ASSERT_TRUE(NavigateToURL(shell(), GURL("about:blank")));
{
TitleWatcher sent_title_watcher(shell()->web_contents(), passed_title);
ASSERT_TRUE(
ExecJsInWebUI("document.title = window.Mojo? 'failed' : 'passed';"));
ASSERT_EQ(passed_title, sent_title_watcher.WaitAndGetTitle());
}
}
IN_PROC_BROWSER_TEST_F(
AttributionInternalsWebUiBrowserTest,
WebUIShownWithPendingAggregatableReports_ReportsDisplayed) {
ASSERT_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)
.SetAttestationToken("abc")
.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(RunOnceCallback<1>(std::vector<AttributionReport>{
ReportBuilder(
AttributionInfoBuilder(SourceBuilder(now)
.SetSourceType(SourceType::kEvent)
.BuildStored())
.Build())
.SetReportTime(now)
.SetAggregatableHistogramContributions(contributions)
.BuildAggregatableAttribution()}));
{
static constexpr char kScript[] = R"(
const table = document.querySelector('#aggregatableReportTable')
.shadowRoot.querySelector('tbody');
const 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[0].children[7]?.innerText === '' &&
table.children[0].children[8]?.innerText === 'aws-cloud' &&
table.children[1].children[2]?.innerText === 'Sent: HTTP 200' &&
table.children[1].children[7]?.innerText === 'abc' &&
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, subtree: true, characterData: true});
)";
ASSERT_TRUE(ExecJsInWebUI(JsReplace(kScript, kCompleteTitle)));
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
ClickRefreshButton();
ASSERT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle());
}
}
IN_PROC_BROWSER_TEST_F(AttributionInternalsWebUiBrowserTest,
TriggersDisplayed) {
ASSERT_TRUE(NavigateToURL(shell(), GURL(kAttributionInternalsUrl)));
const auto create_trigger = [](absl::optional<network::TriggerAttestation>
attestation) {
return AttributionTrigger(
/*reporting_origin=*/*SuitableOrigin::Deserialize("https://r.test"),
attribution_reporting::TriggerRegistration(
FilterPair(/*positive=*/{{{"a", {"b"}}}},
/*negative=*/{{{"g", {"h"}}}}),
/*debug_key=*/1,
{attribution_reporting::AggregatableDedupKey(
/*dedup_key=*/18, FilterPair())},
{
attribution_reporting::EventTriggerData(
/*data=*/2,
/*priority=*/3,
/*dedup_key=*/absl::nullopt,
FilterPair(
/*positive=*/{{{"c", {"d"}}}},
/*negative=*/{})),
attribution_reporting::EventTriggerData(
/*data=*/4,
/*priority=*/5,
/*dedup_key=*/6,
FilterPair(/*positive=*/{}, /*negative=*/{{{"e", {"f"}}}})),
},
{*attribution_reporting::AggregatableTriggerData::Create(
/*key_piece=*/345,
/*source_keys=*/{"a"},
FilterPair(/*positive=*/{},
/*negative=*/{{{"c", {"d"}}}})),
*attribution_reporting::AggregatableTriggerData::Create(
/*key_piece=*/678,
/*source_keys=*/{"b"},
FilterPair(/*positive=*/{}, /*negative=*/{{{"e", {"f"}}}}))},
/*aggregatable_values=*/
*attribution_reporting::AggregatableValues::Create(
{{"a", 123}, {"b", 456}}),
/*debug_reporting=*/false,
::aggregation_service::mojom::AggregationCoordinator::kDefault),
*SuitableOrigin::Deserialize("https://d.test"), std::move(attestation),
/*is_within_fenced_frame=*/false);
};
static constexpr char kScript[] = R"(
const expectedAttestation =
'<dl><dt>Token</dt><dd>abc</dd>' +
'<dt>Report ID</dt><dd>a2ab30b9-d664-4dfc-a9db-85f9729b9a30</dd></dl>';
const table = document.querySelector('#triggerTable')
.shadowRoot.querySelector('tbody');
const obs = new MutationObserver((_, obs) => {
if (table.children.length === 2 &&
table.children[0].children[5]?.innerText === 'Success: Report stored' &&
table.children[0].children[6]?.innerText === 'Success: Report stored' &&
table.children[0].children[1]?.innerText === 'https://d.test' &&
table.children[0].children[2]?.innerText === 'https://r.test' &&
table.children[0].children[3]?.innerText.includes('{') &&
table.children[0].children[4]?.innerText === '' &&
table.children[1].children[4]?.innerText === '123' &&
table.children[1].children[7]?.innerHTML === expectedAttestation) {
obs.disconnect();
document.title = $1;
}
});
obs.observe(table, {childList: true, subtree: true, characterData: true});
)";
ASSERT_TRUE(ExecJsInWebUI(JsReplace(kScript, kCompleteTitle)));
const base::Time now = base::Time::Now();
auto notify_trigger_handled =
[&](const AttributionTrigger& trigger,
AttributionTrigger::EventLevelResult event_status,
AttributionTrigger::AggregatableResult aggregatable_status,
absl::optional<uint64_t> cleared_debug_key = absl::nullopt) {
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(),
/*source=*/SourceBuilder().BuildStored()),
cleared_debug_key);
};
notify_trigger_handled(create_trigger(/*attestation=*/absl::nullopt),
AttributionTrigger::EventLevelResult::kSuccess,
AttributionTrigger::AggregatableResult::kSuccess);
notify_trigger_handled(create_trigger(network::TriggerAttestation::Create(
"abc", "a2ab30b9-d664-4dfc-a9db-85f9729b9a30")),
AttributionTrigger::EventLevelResult::kSuccess,
AttributionTrigger::AggregatableResult::kSuccess,
/*cleared_debug_key=*/123);
// 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) {
ASSERT_TRUE(NavigateToURL(shell(), GURL(kAttributionInternalsUrl)));
EXPECT_CALL(*manager(), GetPendingReportsForInternalUse)
.WillOnce(RunOnceCallback<1>(std::vector<AttributionReport>{
ReportBuilder(
AttributionInfoBuilder(SourceBuilder().BuildStored()).Build())
.SetReportId(
AttributionReport::AggregatableAttributionData::Id(5))
.SetAggregatableHistogramContributions(
{AggregatableHistogramContribution(1, 2)})
.BuildAggregatableAttribution()}))
.WillOnce(RunOnceCallback<1>(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 kScript[] = R"(
const table = document.querySelector('#aggregatableReportTable')
.shadowRoot.querySelector('tbody');
const obs = new MutationObserver((_, obs) => {
if (table.children.length === 1) {
obs.disconnect();
document.title = $1;
}
});
obs.observe(table, {childList: true});
)";
ASSERT_TRUE(ExecJsInWebUI(JsReplace(kScript, 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);
static constexpr char kObserveEmptyReportsTableScript[] = R"(
const table = document.querySelector('#aggregatableReportTable')
.shadowRoot.querySelector('tbody');
const 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, subtree: true, characterData: true});
)";
ASSERT_TRUE(
ExecJsInWebUI(JsReplace(kObserveEmptyReportsTableScript, kSentTitle)));
ASSERT_TRUE(ExecJsInWebUI(R"(
document.querySelector('#aggregatableReportTable')
.shadowRoot.querySelectorAll('input[type="checkbox"]')[1].click();
)"));
ASSERT_TRUE(ExecJsInWebUI(
"document.getElementById('send-aggregatable-reports').click();"));
// The real manager would do this itself, but the test manager requires manual
// triggering.
manager()->NotifyReportsChanged();
EXPECT_EQ(kSentTitle, sent_title_watcher.WaitAndGetTitle());
}
IN_PROC_BROWSER_TEST_F(AttributionInternalsWebUiBrowserTest,
ToggleDebugReports) {
ASSERT_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(RunOnceCallback<1>(std::vector<AttributionReport>{
ReportBuilder(
AttributionInfoBuilder(SourceBuilder(now).BuildStored()).Build())
.SetReportTime(now + base::Hours(1))
.SetPriority(2)
.Build()}));
// By default, debug reports are shown.
{
static constexpr char kScript[] = R"(
const table = document.querySelector('#reportTable')
.shadowRoot.querySelector('tbody');
const label = document.querySelector('#show-debug-event-reports span');
const 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, subtree: true, characterData: true});
obs.observe(label, {childList: true, characterData: true});
)";
ASSERT_TRUE(ExecJsInWebUI(JsReplace(kScript, kCompleteTitle)));
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
ClickRefreshButton();
ASSERT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle());
}
// Toggle checkbox.
ASSERT_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 kScript[] = R"(
const table = document.querySelector('#reportTable')
.shadowRoot.querySelector('tbody');
const label = document.querySelector('#show-debug-event-reports span');
const 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, subtree: true, characterData: true});
obs.observe(label, {childList: true, characterData: true});
)";
ASSERT_TRUE(ExecJsInWebUI(JsReplace(kScript, kCompleteTitle2)));
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle2);
ClickRefreshButton();
ASSERT_EQ(kCompleteTitle2, title_watcher.WaitAndGetTitle());
}
// Toggle checkbox.
ASSERT_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 kScript[] = R"(
const table = document.querySelector('#reportTable').shadowRoot
.querySelector('tbody');
const label = document.querySelector('#show-debug-event-reports span');
const 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, subtree: true, characterData: true});
obs.observe(label, {childList: true, characterData: true});
)";
ASSERT_TRUE(ExecJsInWebUI(JsReplace(kScript, kCompleteTitle3)));
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle3);
ClickRefreshButton();
EXPECT_EQ(kCompleteTitle3, title_watcher.WaitAndGetTitle());
}
}
IN_PROC_BROWSER_TEST_F(AttributionInternalsWebUiBrowserTest,
VerboseDebugReport) {
ASSERT_TRUE(NavigateToURL(shell(), GURL(kAttributionInternalsUrl)));
absl::optional<AttributionDebugReport> report =
AttributionDebugReport::Create(
SourceBuilder().SetDebugReporting(true).Build(),
/*is_debug_cookie_set=*/true,
StoreSourceResult(StorableSource::Result::kInternalError));
ASSERT_TRUE(report);
static constexpr char kScript[] = R"(
const table = document.querySelector('#debugReportTable')
.shadowRoot.querySelector('tbody');
const url = 'https://report.test/.well-known/attribution-reporting/debug/verbose';
const obs = new MutationObserver((_, obs) => {
if (table.children.length === 1 &&
table.children[0].children[1]?.innerText === url &&
table.children[0].children[2]?.innerText === 'HTTP 200' &&
table.children[0].children[3]?.innerText.includes('source-unknown-error')
) {
obs.disconnect();
document.title = $1;
}
});
obs.observe(table, {childList: true, subtree: true, characterData: true});
)";
ASSERT_TRUE(ExecJsInWebUI(JsReplace(kScript, kCompleteTitle)));
TitleWatcher title_watcher(shell()->web_contents(), kCompleteTitle);
manager()->NotifyDebugReportSent(*report, /*status=*/200, base::Time::Now());
EXPECT_EQ(kCompleteTitle, title_watcher.WaitAndGetTitle());
}
} // namespace content