blob: de67b912cd1945a7072c6d1fd5c9794de2ce7efb [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_manager_impl.h"
#include <initializer_list>
#include <memory>
#include <utility>
#include <vector>
#include "base/check.h"
#include "base/containers/circular_deque.h"
#include "base/containers/flat_set.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/run_loop.h"
#include "base/scoped_observation.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/task/updateable_sequenced_task_runner.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/time/time.h"
#include "base/values.h"
#include "build/build_config.h"
#include "components/attribution_reporting/suitable_origin.h"
#include "content/browser/aggregation_service/aggregatable_report.h"
#include "content/browser/aggregation_service/aggregation_service.h"
#include "content/browser/aggregation_service/aggregation_service_test_utils.h"
#include "content/browser/attribution_reporting/aggregatable_attribution_utils.h"
#include "content/browser/attribution_reporting/aggregatable_histogram_contribution.h"
#include "content/browser/attribution_reporting/attribution_cookie_checker.h"
#include "content/browser/attribution_reporting/attribution_debug_report.h"
#include "content/browser/attribution_reporting/attribution_observer.h"
#include "content/browser/attribution_reporting/attribution_observer_types.h"
#include "content/browser/attribution_reporting/attribution_report.h"
#include "content/browser/attribution_reporting/attribution_report_sender.h"
#include "content/browser/attribution_reporting/attribution_storage.h"
#include "content/browser/attribution_reporting/attribution_storage_delegate.h"
#include "content/browser/attribution_reporting/attribution_test_utils.h"
#include "content/browser/attribution_reporting/attribution_trigger.h"
#include "content/browser/attribution_reporting/common_source_info.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/browsing_data_filter_builder.h"
#include "content/public/browser/global_routing_id.h"
#include "content/public/browser/network_service_instance.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/test_browser_context.h"
#include "content/public/test/test_utils.h"
#include "services/network/public/cpp/features.h"
#include "services/network/test/test_network_connection_tracker.h"
#include "storage/browser/test/mock_special_storage_policy.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "url/gurl.h"
#include "url/origin.h"
namespace content {
namespace {
using ::attribution_reporting::SuitableOrigin;
using ::testing::_;
using ::testing::AllOf;
using ::testing::AnyOf;
using ::testing::ElementsAre;
using ::testing::Expectation;
using ::testing::Field;
using ::testing::Ge;
using ::testing::InSequence;
using ::testing::IsEmpty;
using ::testing::IsNull;
using ::testing::Le;
using ::testing::Optional;
using ::testing::Pointee;
using ::testing::Return;
using ::testing::SizeIs;
using ::testing::UnorderedElementsAre;
using Checkpoint = ::testing::MockFunction<void(int step)>;
using DebugReportSentCallback =
::content::AttributionReportSender::DebugReportSentCallback;
using ReportSentCallback =
::content::AttributionReportSender::ReportSentCallback;
constexpr size_t kMaxPendingEvents = 5;
const GlobalRenderFrameHostId kFrameId = {0, 1};
constexpr AttributionStorageDelegate::OfflineReportDelayConfig
kDefaultOfflineReportDelay{
.min = base::Minutes(0),
.max = base::Minutes(1),
};
auto InvokeReportSentCallback(SendResult::Status status) {
return [=](AttributionReport report, bool is_debug_report,
ReportSentCallback callback) {
std::move(callback).Run(std::move(report), SendResult(status));
};
}
AggregatableReport CreateExampleAggregatableReport() {
std::vector<AggregatableReport::AggregationServicePayload> payloads;
payloads.emplace_back(/*payload=*/kABCD1234AsBytes,
/*key_id=*/"key_1",
/*debug_cleartext_payload=*/absl::nullopt);
payloads.emplace_back(/*payload=*/kEFGH5678AsBytes,
/*key_id=*/"key_2",
/*debug_cleartext_payload=*/absl::nullopt);
base::Value::Dict additional_fields;
additional_fields.Set("source_registration_time", "1234569600");
additional_fields.Set(
"attribution_destination",
url::Origin::Create(GURL("https://example.destination")).Serialize());
AggregatableReportSharedInfo shared_info(
base::Time::FromJavaTime(1234567890123), DefaultExternalReportID(),
/*reporting_origin=*/
url::Origin::Create(GURL("https://example.reporting")),
AggregatableReportSharedInfo::DebugMode::kDisabled,
std::move(additional_fields),
/*api_version=*/"",
/*api_identifier=*/"attribution-reporting");
return AggregatableReport(std::move(payloads), shared_info.SerializeAsJson(),
/*debug_key=*/absl::nullopt);
}
// Time after impression that a conversion can first be sent. See
// AttributionStorageDelegateImpl::GetReportTimeForConversion().
constexpr base::TimeDelta kFirstReportingWindow = base::Days(2);
// Give impressions a sufficiently long expiry.
constexpr base::TimeDelta kImpressionExpiry = base::Days(30);
class MockReportSender : public AttributionReportSender {
public:
MOCK_METHOD(void,
SendReport,
(AttributionReport report,
bool is_debug_report,
ReportSentCallback callback),
(override));
MOCK_METHOD(void,
SendReport,
(AttributionDebugReport report, DebugReportSentCallback callback),
(override));
};
class MockCookieChecker : public AttributionCookieChecker {
public:
~MockCookieChecker() override { EXPECT_THAT(callbacks_, IsEmpty()); }
// AttributionManagerImpl::CookieChecker:
void IsDebugCookieSet(const url::Origin& origin,
base::OnceCallback<void(bool)> callback) override {
if (defer_callbacks_) {
callbacks_.push_back(std::move(callback));
} else {
std::move(callback).Run(origins_with_debug_cookie_set_.contains(origin));
}
}
void AddOriginWithDebugCookieSet(url::Origin origin) {
origins_with_debug_cookie_set_.insert(std::move(origin));
}
void DeferCallbacks() { defer_callbacks_ = true; }
void RunNextDeferredCallback(bool is_debug_cookie_set) {
if (!callbacks_.empty()) {
std::move(callbacks_.front()).Run(is_debug_cookie_set);
callbacks_.pop_front();
}
}
private:
base::flat_set<url::Origin> origins_with_debug_cookie_set_;
bool defer_callbacks_ = false;
base::circular_deque<base::OnceCallback<void(bool)>> callbacks_;
};
} // namespace
class AttributionManagerImplTest : public testing::Test {
public:
AttributionManagerImplTest()
: task_environment_(base::test::TaskEnvironment::TimeSource::MOCK_TIME),
browser_context_(std::make_unique<TestBrowserContext>()),
mock_storage_policy_(
base::MakeRefCounted<storage::MockSpecialStoragePolicy>()) {
// This UMA records a sample every 30s via a periodic task which
// interacts poorly with TaskEnvironment::FastForward using day long
// delays (we need to run the uma update every 30s for that
// interval)
scoped_feature_list_.InitAndDisableFeature(
network::features::kGetCookiesStringUma);
}
void SetUp() override {
EXPECT_TRUE(dir_.CreateUniqueTempDir());
content::SetNetworkConnectionTrackerForTesting(
network::TestNetworkConnectionTracker::GetInstance());
storage_task_runner_ =
base::ThreadPool::CreateUpdateableSequencedTaskRunner(
{base::TaskPriority::BEST_EFFORT, base::MayBlock(),
base::TaskShutdownBehavior::BLOCK_SHUTDOWN,
base::ThreadPolicy::MUST_USE_FOREGROUND});
CreateManager();
CreateAggregationService();
}
void CreateManager() {
CHECK(!attribution_manager_);
auto storage_delegate = std::make_unique<ConfigurableStorageDelegate>();
storage_delegate->set_report_delay(kFirstReportingWindow);
storage_delegate->set_max_attributions_per_source(3);
storage_delegate->set_offline_report_delay_config(
kDefaultOfflineReportDelay);
ConfigureStorageDelegate(*storage_delegate);
// From this point on, the delegate will only be accessed on storage's
// sequence.
storage_delegate->DetachFromSequence();
auto cookie_checker = std::make_unique<MockCookieChecker>();
cookie_checker_ = cookie_checker.get();
auto report_sender = std::make_unique<MockReportSender>();
report_sender_ = report_sender.get();
attribution_manager_ = AttributionManagerImpl::CreateForTesting(
dir_.GetPath(), kMaxPendingEvents, mock_storage_policy_,
std::move(storage_delegate), std::move(cookie_checker),
std::move(report_sender),
static_cast<StoragePartitionImpl*>(
browser_context_->GetDefaultStoragePartition()),
storage_task_runner_);
}
void ShutdownManager() {
cookie_checker_ = nullptr;
report_sender_ = nullptr;
attribution_manager_.reset();
}
void CreateAggregationService() {
auto* partition = static_cast<StoragePartitionImpl*>(
browser_context_->GetDefaultStoragePartition());
auto aggregation_service = std::make_unique<MockAggregationService>();
aggregation_service_ = aggregation_service.get();
partition->OverrideAggregationServiceForTesting(
std::move(aggregation_service));
}
void ShutdownAggregationService() {
auto* partition = static_cast<StoragePartitionImpl*>(
browser_context_->GetDefaultStoragePartition());
aggregation_service_ = nullptr;
partition->OverrideAggregationServiceForTesting(nullptr);
}
std::vector<StoredSource> StoredSources() {
std::vector<StoredSource> result;
base::RunLoop loop;
attribution_manager_->GetActiveSourcesForWebUI(
base::BindLambdaForTesting([&](std::vector<StoredSource> sources) {
result = std::move(sources);
loop.Quit();
}));
loop.Run();
return result;
}
std::vector<AttributionReport> StoredReports() {
return GetAttributionReportsForTesting(attribution_manager_.get());
}
void ForceGetReportsToSend() { attribution_manager_->GetReportsToSend(); }
void SetConnectionTypeAndWaitForObserversToBeNotified(
network::mojom::ConnectionType connection_type) {
network::TestNetworkConnectionTracker::GetInstance()->SetConnectionType(
connection_type);
// Ensure that the network connection observers have been notified before
// this call returns.
task_environment_.RunUntilIdle();
}
protected:
// Override this in order to modify the delegate before it is passed
// irretrievably to storage.
virtual void ConfigureStorageDelegate(ConfigurableStorageDelegate&) const {}
base::test::ScopedFeatureList scoped_feature_list_;
base::ScopedTempDir dir_;
BrowserTaskEnvironment task_environment_;
std::unique_ptr<TestBrowserContext> browser_context_;
std::unique_ptr<AttributionManagerImpl> attribution_manager_;
scoped_refptr<storage::MockSpecialStoragePolicy> mock_storage_policy_;
raw_ptr<MockCookieChecker> cookie_checker_;
raw_ptr<MockReportSender> report_sender_;
raw_ptr<MockAggregationService> aggregation_service_;
scoped_refptr<base::UpdateableSequencedTaskRunner> storage_task_runner_;
};
TEST_F(AttributionManagerImplTest, ImpressionRegistered_ReturnedToWebUI) {
SourceBuilder builder;
builder.SetExpiry(kImpressionExpiry).SetSourceEventId(100);
attribution_manager_->HandleSource(builder.Build(), kFrameId);
EXPECT_THAT(StoredSources(),
ElementsAre(CommonSourceInfoIs(builder.BuildCommonInfo())));
}
TEST_F(AttributionManagerImplTest, ExpiredImpression_NotReturnedToWebUI) {
attribution_manager_->HandleSource(SourceBuilder()
.SetExpiry(kImpressionExpiry)
.SetSourceEventId(100)
.Build(),
kFrameId);
task_environment_.FastForwardBy(2 * kImpressionExpiry);
EXPECT_THAT(StoredSources(), IsEmpty());
}
TEST_F(AttributionManagerImplTest, ImpressionConverted_ReportReturnedToWebUI) {
SourceBuilder builder;
builder.SetExpiry(kImpressionExpiry).SetSourceEventId(100);
attribution_manager_->HandleSource(builder.Build(), kFrameId);
auto conversion = TriggerBuilder().SetTriggerData(5).Build();
attribution_manager_->HandleTrigger(conversion, kFrameId);
AttributionReport expected_report =
ReportBuilder(AttributionInfoBuilder(
builder.BuildStored(),
/*context_origin=*/conversion.destination_origin())
.SetTime(base::Time::Now())
.Build())
.SetTriggerData(5)
.SetReportTime(base::Time::Now() + kFirstReportingWindow)
.Build();
// The external report ID is randomly generated by the storage delegate,
// so zero it out here to avoid flakiness.
std::vector<AttributionReport> reports = StoredReports();
for (auto& report : reports) {
report.SetExternalReportIdForTesting(DefaultExternalReportID());
}
EXPECT_THAT(reports, ElementsAre(expected_report));
}
TEST_F(AttributionManagerImplTest, ImpressionConverted_ReportSent) {
base::HistogramTester histograms;
Checkpoint checkpoint;
{
InSequence seq;
EXPECT_CALL(*report_sender_, SendReport(_, /*is_debug_report=*/false, _))
.Times(0);
EXPECT_CALL(checkpoint, Call(1));
EXPECT_CALL(*report_sender_, SendReport(_, /*is_debug_report=*/false, _));
}
attribution_manager_->HandleSource(
SourceBuilder().SetExpiry(kImpressionExpiry).Build(), kFrameId);
attribution_manager_->HandleTrigger(DefaultTrigger(), kFrameId);
// Make sure the report is not sent earlier than its report time.
task_environment_.FastForwardBy(kFirstReportingWindow -
base::Microseconds(1));
checkpoint.Call(1);
task_environment_.FastForwardBy(base::Microseconds(1));
histograms.ExpectUniqueSample("Conversions.RegisterImpressionAllowed", true,
1);
histograms.ExpectUniqueSample("Conversions.RegisterConversionAllowed", true,
1);
}
TEST_F(AttributionManagerImplTest,
MultipleReportsWithSameReportTime_AllSentSimultaneously) {
const GURL url_a(
"https://a.example/.well-known/attribution-reporting/"
"report-event-attribution");
const GURL url_b(
"https://b.example/.well-known/attribution-reporting/"
"report-event-attribution");
const GURL url_c(
"https://c.example/.well-known/attribution-reporting/"
"report-event-attribution");
const auto origin_a = *SuitableOrigin::Create(url_a);
const auto origin_b = *SuitableOrigin::Create(url_b);
const auto origin_c = *SuitableOrigin::Create(url_c);
std::vector<AttributionReport> sent_reports;
Checkpoint checkpoint;
{
InSequence seq;
EXPECT_CALL(*report_sender_, SendReport(_, /*is_debug_report=*/false, _))
.Times(0);
EXPECT_CALL(checkpoint, Call(1));
EXPECT_CALL(*report_sender_, SendReport(_, /*is_debug_report=*/false, _))
.WillRepeatedly([&](AttributionReport report, bool is_debug_report,
ReportSentCallback callback) {
sent_reports.push_back(std::move(report));
});
}
attribution_manager_->HandleSource(SourceBuilder()
.SetExpiry(kImpressionExpiry)
.SetReportingOrigin(origin_a)
.Build(),
kFrameId);
attribution_manager_->HandleTrigger(
TriggerBuilder().SetReportingOrigin(origin_a).Build(), kFrameId);
attribution_manager_->HandleSource(SourceBuilder()
.SetExpiry(kImpressionExpiry)
.SetReportingOrigin(origin_b)
.Build(),
kFrameId);
attribution_manager_->HandleTrigger(
TriggerBuilder().SetReportingOrigin(origin_b).Build(), kFrameId);
attribution_manager_->HandleSource(SourceBuilder()
.SetExpiry(kImpressionExpiry)
.SetReportingOrigin(origin_c)
.Build(),
kFrameId);
attribution_manager_->HandleTrigger(
TriggerBuilder().SetReportingOrigin(origin_c).Build(), kFrameId);
EXPECT_THAT(StoredReports(), SizeIs(3));
// Make sure the reports are not sent earlier than their report time.
task_environment_.FastForwardBy(kFirstReportingWindow -
base::Microseconds(1));
checkpoint.Call(1);
task_environment_.FastForwardBy(base::Microseconds(1));
// The 3 reports can be sent in any order due to the `base::RandomShuffle()`
// in `AttributionManagerImpl::OnGetReportsToSend()`.
EXPECT_THAT(sent_reports,
UnorderedElementsAre(ReportURLIs(url_a), ReportURLIs(url_b),
ReportURLIs(url_c)));
}
TEST_F(AttributionManagerImplTest,
MultipleReportsWithDifferentReportTimes_SentInSequence) {
const GURL url_a(
"https://a.example/.well-known/attribution-reporting/"
"report-event-attribution");
const GURL url_b(
"https://b.example/.well-known/attribution-reporting/"
"report-event-attribution");
const auto origin_a = *SuitableOrigin::Create(url_a);
const auto origin_b = *SuitableOrigin::Create(url_b);
Checkpoint checkpoint;
{
InSequence seq;
EXPECT_CALL(*report_sender_, SendReport(_, /*is_debug_report=*/false, _))
.Times(0);
EXPECT_CALL(checkpoint, Call(1));
EXPECT_CALL(*report_sender_,
SendReport(ReportURLIs(url_a), /*is_debug_report=*/false, _));
EXPECT_CALL(checkpoint, Call(2));
EXPECT_CALL(*report_sender_,
SendReport(ReportURLIs(url_b), /*is_debug_report=*/false, _));
}
attribution_manager_->HandleSource(SourceBuilder()
.SetExpiry(kImpressionExpiry)
.SetReportingOrigin(origin_a)
.Build(),
kFrameId);
attribution_manager_->HandleTrigger(
TriggerBuilder().SetReportingOrigin(origin_a).Build(), kFrameId);
task_environment_.FastForwardBy(base::Microseconds(1));
attribution_manager_->HandleSource(SourceBuilder()
.SetExpiry(kImpressionExpiry)
.SetReportingOrigin(origin_b)
.Build(),
kFrameId);
attribution_manager_->HandleTrigger(
TriggerBuilder().SetReportingOrigin(origin_b).Build(), kFrameId);
EXPECT_THAT(StoredReports(), SizeIs(2));
// Make sure the reports are not sent earlier than their report time.
task_environment_.FastForwardBy(kFirstReportingWindow -
base::Microseconds(2));
checkpoint.Call(1);
task_environment_.FastForwardBy(base::Microseconds(1));
checkpoint.Call(2);
task_environment_.FastForwardBy(base::Microseconds(1));
}
TEST_F(AttributionManagerImplTest, SenderStillHandlingReport_NotSentAgain) {
Checkpoint checkpoint;
{
InSequence seq;
EXPECT_CALL(*report_sender_, SendReport(_, /*is_debug_report=*/false, _));
EXPECT_CALL(checkpoint, Call(1));
EXPECT_CALL(*report_sender_, SendReport(_, /*is_debug_report=*/false, _))
.Times(0);
}
attribution_manager_->HandleSource(
SourceBuilder().SetExpiry(kImpressionExpiry).Build(), kFrameId);
attribution_manager_->HandleTrigger(DefaultTrigger(), kFrameId);
task_environment_.FastForwardBy(kFirstReportingWindow);
checkpoint.Call(1);
// The sender hasn't invoked the callback, so the manager shouldn't try to
// send the report again.
ForceGetReportsToSend();
}
TEST_F(AttributionManagerImplTest,
QueuedReportFailedWithShouldRetry_QueuedAgain) {
base::HistogramTester histograms;
Checkpoint checkpoint;
{
InSequence seq;
EXPECT_CALL(*report_sender_, SendReport(_, /*is_debug_report=*/false, _))
.WillOnce(
InvokeReportSentCallback(SendResult::Status::kTransientFailure));
EXPECT_CALL(checkpoint, Call(1));
EXPECT_CALL(*report_sender_, SendReport(_, /*is_debug_report=*/false, _))
.WillOnce(
InvokeReportSentCallback(SendResult::Status::kTransientFailure));
EXPECT_CALL(checkpoint, Call(2));
EXPECT_CALL(*report_sender_, SendReport(_, /*is_debug_report=*/false, _))
.WillOnce(
InvokeReportSentCallback(SendResult::Status::kTransientFailure));
}
attribution_manager_->HandleSource(
SourceBuilder().SetExpiry(kImpressionExpiry).Build(), kFrameId);
attribution_manager_->HandleTrigger(DefaultTrigger(), kFrameId);
task_environment_.FastForwardBy(kFirstReportingWindow);
checkpoint.Call(1);
// First report delay.
task_environment_.FastForwardBy(base::Minutes(5));
checkpoint.Call(2);
// Second report delay.
task_environment_.FastForwardBy(base::Minutes(15));
// kFailed = 1.
histograms.ExpectUniqueSample("Conversions.ReportSendOutcome3", 1, 1);
histograms.ExpectTotalCount(
"Conversions.TimeFromTriggerToReportSentSuccessfully", 0);
}
TEST_F(AttributionManagerImplTest, RetryLogicOverridesGetReportTimer) {
const GURL url_a(
"https://a.example/.well-known/attribution-reporting/"
"report-event-attribution");
const GURL url_b(
"https://b.example/.well-known/attribution-reporting/"
"report-event-attribution");
const auto origin_a = *SuitableOrigin::Create(url_a);
const auto origin_b = *SuitableOrigin::Create(url_b);
Checkpoint checkpoint;
{
InSequence seq;
EXPECT_CALL(*report_sender_,
SendReport(ReportURLIs(url_a), /*is_debug_report=*/false, _))
.Times(0);
EXPECT_CALL(checkpoint, Call(1));
EXPECT_CALL(*report_sender_,
SendReport(ReportURLIs(url_a), /*is_debug_report=*/false, _))
.WillOnce(
InvokeReportSentCallback(SendResult::Status::kTransientFailure));
EXPECT_CALL(checkpoint, Call(2));
EXPECT_CALL(*report_sender_,
SendReport(ReportURLIs(url_a), /*is_debug_report=*/false, _));
}
attribution_manager_->HandleSource(SourceBuilder()
.SetExpiry(kImpressionExpiry)
.SetReportingOrigin(origin_a)
.Build(),
kFrameId);
attribution_manager_->HandleTrigger(
TriggerBuilder().SetReportingOrigin(origin_a).Build(), kFrameId);
task_environment_.FastForwardBy(base::Minutes(10));
attribution_manager_->HandleSource(SourceBuilder()
.SetExpiry(kImpressionExpiry)
.SetReportingOrigin(origin_b)
.Build(),
kFrameId);
attribution_manager_->HandleTrigger(
TriggerBuilder().SetReportingOrigin(origin_b).Build(), kFrameId);
EXPECT_THAT(StoredReports(), SizeIs(2));
checkpoint.Call(1);
// Because this report will be retried at its original report time + 5
// minutes, the get-reports timer, which was originally scheduled to run at
// the second report's report time, should be overridden to run earlier.
task_environment_.FastForwardBy(kFirstReportingWindow - base::Minutes(10));
checkpoint.Call(2);
task_environment_.FastForwardBy(base::Minutes(5));
}
TEST_F(AttributionManagerImplTest,
QueuedReportFailedWithoutShouldRetry_NotQueuedAgain) {
base::HistogramTester histograms;
attribution_manager_->HandleSource(
SourceBuilder().SetExpiry(kImpressionExpiry).Build(), kFrameId);
attribution_manager_->HandleTrigger(DefaultTrigger(), kFrameId);
EXPECT_THAT(StoredReports(), SizeIs(1));
MockAttributionObserver observer;
base::ScopedObservation<AttributionManager, AttributionObserver> observation(
&observer);
observation.Observe(attribution_manager_.get());
// Ensure that observers are notified after the report is deleted.
EXPECT_CALL(observer, OnSourcesChanged).Times(0);
EXPECT_CALL(observer, OnReportsChanged);
EXPECT_CALL(*report_sender_, SendReport(_, /*is_debug_report=*/false, _))
.WillOnce(InvokeReportSentCallback(SendResult::Status::kFailure));
task_environment_.FastForwardBy(kFirstReportingWindow);
EXPECT_THAT(StoredReports(), IsEmpty());
// kFailed = 1.
histograms.ExpectUniqueSample("Conversions.ReportSendOutcome3", 1, 1);
histograms.ExpectTotalCount(
"Conversions.TimeFromTriggerToReportSentSuccessfully", 0);
}
TEST_F(AttributionManagerImplTest, QueuedReportAlwaysFails_StopsSending) {
base::HistogramTester histograms;
Checkpoint checkpoint;
{
InSequence seq;
EXPECT_CALL(*report_sender_, SendReport(_, /*is_debug_report=*/false, _))
.Times(0);
EXPECT_CALL(checkpoint, Call(1));
EXPECT_CALL(*report_sender_, SendReport(_, /*is_debug_report=*/false, _))
.WillOnce(
InvokeReportSentCallback(SendResult::Status::kTransientFailure));
EXPECT_CALL(checkpoint, Call(2));
EXPECT_CALL(*report_sender_, SendReport(_, /*is_debug_report=*/false, _))
.WillOnce(
InvokeReportSentCallback(SendResult::Status::kTransientFailure));
EXPECT_CALL(checkpoint, Call(3));
EXPECT_CALL(*report_sender_, SendReport(_, /*is_debug_report=*/false, _))
.WillOnce(
InvokeReportSentCallback(SendResult::Status::kTransientFailure));
}
MockAttributionObserver observer;
base::ScopedObservation<AttributionManager, AttributionObserver> observation(
&observer);
observation.Observe(attribution_manager_.get());
EXPECT_CALL(observer,
OnReportSent(_, /*is_debug_report=*/false,
Field(&SendResult::status,
SendResult::Status::kTransientFailure)));
attribution_manager_->HandleSource(
SourceBuilder().SetExpiry(kImpressionExpiry).Build(), kFrameId);
attribution_manager_->HandleTrigger(DefaultTrigger(), kFrameId);
task_environment_.FastForwardBy(kFirstReportingWindow -
base::Milliseconds(1));
checkpoint.Call(1);
// The report is sent at its expected report time.
task_environment_.FastForwardBy(base::Milliseconds(1));
checkpoint.Call(2);
// The report is sent at the first retry time of +5 minutes.
task_environment_.FastForwardBy(base::Minutes(5));
checkpoint.Call(3);
// The report is sent at the second retry time of +15 minutes.
task_environment_.FastForwardBy(base::Minutes(15));
// At this point, the report has reached the maximum number of attempts and it
// should no longer be present in the DB.
EXPECT_THAT(StoredReports(), IsEmpty());
// kFailed = 1.
histograms.ExpectUniqueSample("Conversions.ReportSendOutcome3", 1, 1);
histograms.ExpectTotalCount(
"Conversions.TimeFromTriggerToReportSentSuccessfully", 0);
}
TEST_F(AttributionManagerImplTest, ReportExpiredAtStartup_Sent) {
attribution_manager_->HandleSource(
SourceBuilder().SetExpiry(kImpressionExpiry).Build(), kFrameId);
attribution_manager_->HandleTrigger(DefaultTrigger(), kFrameId);
ShutdownManager();
// Fast-forward past the reporting window and past report expiry.
task_environment_.FastForwardBy(kFirstReportingWindow);
task_environment_.FastForwardBy(base::Days(100));
// Simulate startup and ensure the report is sent before being expired.
// Advance by the max offline report delay, per
// `AttributionStorageDelegate::GetOfflineReportDelayConfig()`.
CreateManager();
EXPECT_CALL(*report_sender_, SendReport(_, /*is_debug_report=*/false, _));
task_environment_.FastForwardBy(kDefaultOfflineReportDelay.max);
}
TEST_F(AttributionManagerImplTest, ReportSent_Deleted) {
base::HistogramTester histograms;
attribution_manager_->HandleSource(
SourceBuilder().SetExpiry(kImpressionExpiry).Build(), kFrameId);
attribution_manager_->HandleTrigger(DefaultTrigger(), kFrameId);
EXPECT_CALL(*report_sender_, SendReport(_, /*is_debug_report=*/false, _))
.WillOnce(InvokeReportSentCallback(SendResult::Status::kSent));
task_environment_.FastForwardBy(kFirstReportingWindow);
EXPECT_THAT(StoredReports(), IsEmpty());
// kSent = 0.
histograms.ExpectUniqueSample("Conversions.ReportSendOutcome3", 0, 1);
}
TEST_F(AttributionManagerImplTest, QueuedReportSent_ObserversNotified) {
base::HistogramTester histograms;
EXPECT_CALL(*report_sender_, SendReport(_, /*is_debug_report=*/false, _))
.WillOnce(InvokeReportSentCallback(SendResult::Status::kSent))
.WillOnce(InvokeReportSentCallback(SendResult::Status::kDropped))
.WillOnce(InvokeReportSentCallback(SendResult::Status::kSent))
.WillOnce(
InvokeReportSentCallback(SendResult::Status::kTransientFailure));
MockAttributionObserver observer;
base::ScopedObservation<AttributionManager, AttributionObserver> observation(
&observer);
observation.Observe(attribution_manager_.get());
EXPECT_CALL(observer, OnReportSent(ReportSourceIs(SourceEventIdIs(1u)),
/*is_debug_report=*/false, _));
EXPECT_CALL(observer, OnReportSent(ReportSourceIs(SourceEventIdIs(2u)),
/*is_debug_report=*/false, _));
EXPECT_CALL(observer, OnReportSent(ReportSourceIs(SourceEventIdIs(3u)),
/*is_debug_report=*/false, _));
attribution_manager_->HandleSource(
SourceBuilder().SetSourceEventId(1).SetExpiry(kImpressionExpiry).Build(),
kFrameId);
attribution_manager_->HandleTrigger(DefaultTrigger(), kFrameId);
task_environment_.FastForwardBy(kFirstReportingWindow);
// This one should be stored, as its status is `kDropped`.
attribution_manager_->HandleSource(
SourceBuilder().SetSourceEventId(2).SetExpiry(kImpressionExpiry).Build(),
kFrameId);
attribution_manager_->HandleTrigger(DefaultTrigger(), kFrameId);
task_environment_.FastForwardBy(kFirstReportingWindow);
attribution_manager_->HandleSource(
SourceBuilder().SetSourceEventId(3).SetExpiry(kImpressionExpiry).Build(),
kFrameId);
attribution_manager_->HandleTrigger(DefaultTrigger(), kFrameId);
task_environment_.FastForwardBy(kFirstReportingWindow);
// This one shouldn't be stored, as it will be retried.
attribution_manager_->HandleSource(
SourceBuilder().SetSourceEventId(4).SetExpiry(kImpressionExpiry).Build(),
kFrameId);
attribution_manager_->HandleTrigger(DefaultTrigger(), kFrameId);
task_environment_.FastForwardBy(kFirstReportingWindow);
// kSent = 0.
histograms.ExpectBucketCount("Conversions.ReportSendOutcome3", 0, 2);
// kFailed = 1.
histograms.ExpectBucketCount("Conversions.ReportSendOutcome3", 1, 0);
// kDropped = 2.
histograms.ExpectBucketCount("Conversions.ReportSendOutcome3", 2, 1);
}
TEST_F(AttributionManagerImplTest, TriggerHandled_ObserversNotified) {
MockAttributionObserver observer;
base::ScopedObservation<AttributionManager, AttributionObserver> observation(
&observer);
observation.Observe(attribution_manager_.get());
Checkpoint checkpoint;
{
InSequence seq;
EXPECT_CALL(
observer,
OnTriggerHandled(_, _,
CreateReportEventLevelStatusIs(
AttributionTrigger::EventLevelResult::kSuccess)))
.Times(3);
EXPECT_CALL(checkpoint, Call(1));
EXPECT_CALL(
observer,
OnTriggerHandled(_, _,
AllOf(ReplacedEventLevelReportIs(Optional(
EventLevelDataIs(TriggerPriorityIs(1)))),
CreateReportEventLevelStatusIs(
AttributionTrigger::EventLevelResult::
kSuccessDroppedLowerPriority))));
EXPECT_CALL(checkpoint, Call(2));
EXPECT_CALL(
observer,
OnTriggerHandled(
_, _,
AllOf(ReplacedEventLevelReportIs(absl::nullopt),
CreateReportEventLevelStatusIs(
AttributionTrigger::EventLevelResult::kPriorityTooLow))));
EXPECT_CALL(checkpoint, Call(3));
EXPECT_CALL(
observer,
OnTriggerHandled(_, _,
AllOf(ReplacedEventLevelReportIs(Optional(
EventLevelDataIs(TriggerPriorityIs(2)))),
CreateReportEventLevelStatusIs(
AttributionTrigger::EventLevelResult::
kSuccessDroppedLowerPriority))));
EXPECT_CALL(
observer,
OnTriggerHandled(_, _,
AllOf(ReplacedEventLevelReportIs(Optional(
EventLevelDataIs(TriggerPriorityIs(3)))),
CreateReportEventLevelStatusIs(
AttributionTrigger::EventLevelResult::
kSuccessDroppedLowerPriority))));
}
attribution_manager_->HandleSource(
SourceBuilder().SetExpiry(kImpressionExpiry).Build(), kFrameId);
EXPECT_THAT(StoredSources(), SizeIs(1));
// `kNavigation` sources can have 3 reports, so none of these should result in
// a dropped report.
for (int i = 1; i <= 3; i++) {
attribution_manager_->HandleTrigger(TriggerBuilder().SetPriority(i).Build(),
kFrameId);
EXPECT_THAT(StoredReports(), SizeIs(i));
}
checkpoint.Call(1);
{
// This should replace the report with priority 1.
attribution_manager_->HandleTrigger(TriggerBuilder().SetPriority(4).Build(),
kFrameId);
EXPECT_THAT(StoredReports(), SizeIs(3));
}
checkpoint.Call(2);
{
// This should be dropped, as it has a lower priority than all stored
// reports.
attribution_manager_->HandleTrigger(
TriggerBuilder().SetPriority(-5).Build(), kFrameId);
EXPECT_THAT(StoredReports(), SizeIs(3));
}
checkpoint.Call(3);
{
// These should replace the reports with priority 2 and 3.
attribution_manager_->HandleTrigger(TriggerBuilder().SetPriority(5).Build(),
kFrameId);
attribution_manager_->HandleTrigger(TriggerBuilder().SetPriority(6).Build(),
kFrameId);
EXPECT_THAT(StoredReports(), SizeIs(3));
}
}
// This functionality is tested more thoroughly in the AttributionStorageSql
// unit tests. Here, just test to make sure the basic control flow is working.
TEST_F(AttributionManagerImplTest, ClearData) {
for (bool match_url : {true, false}) {
base::Time start = base::Time::Now();
attribution_manager_->HandleSource(
SourceBuilder(start).SetExpiry(kImpressionExpiry).Build(), kFrameId);
attribution_manager_->HandleTrigger(DefaultTrigger(), kFrameId);
base::RunLoop run_loop;
attribution_manager_->ClearData(
start, start + base::Minutes(1),
base::BindLambdaForTesting(
[match_url](const blink::StorageKey&) { return match_url; }),
/*filter_builder=*/nullptr,
/*delete_rate_limit_data=*/true, run_loop.QuitClosure());
run_loop.Run();
size_t expected_reports = match_url ? 0u : 1u;
EXPECT_THAT(StoredReports(), SizeIs(expected_reports));
}
}
TEST_F(AttributionManagerImplTest, ConversionsSentFromUI_ReportedImmediately) {
Checkpoint checkpoint;
{
InSequence seq;
EXPECT_CALL(*report_sender_, SendReport(_, /*is_debug_report=*/false, _))
.Times(0);
EXPECT_CALL(checkpoint, Call(1));
EXPECT_CALL(*report_sender_, SendReport(_, /*is_debug_report=*/false, _));
}
attribution_manager_->HandleSource(
SourceBuilder().SetExpiry(kImpressionExpiry).Build(), kFrameId);
attribution_manager_->HandleTrigger(DefaultTrigger(), kFrameId);
std::vector<AttributionReport> reports = StoredReports();
EXPECT_THAT(reports, SizeIs(1));
checkpoint.Call(1);
attribution_manager_->SendReportsForWebUI({reports.front().ReportId()},
base::DoNothing());
task_environment_.FastForwardBy(base::TimeDelta());
}
TEST_F(AttributionManagerImplTest,
ConversionsSentFromUI_CallbackInvokedWhenAllDone) {
std::vector<ReportSentCallback> report_sent_callbacks;
std::vector<AttributionReport> sent_reports;
Checkpoint checkpoint;
{
InSequence seq;
EXPECT_CALL(*report_sender_, SendReport(_, /*is_debug_report=*/false, _))
.Times(0);
EXPECT_CALL(checkpoint, Call(1));
EXPECT_CALL(*report_sender_, SendReport(_, /*is_debug_report=*/false, _))
.WillRepeatedly([&](AttributionReport report, bool is_debug_report,
ReportSentCallback callback) {
report_sent_callbacks.push_back(std::move(callback));
sent_reports.push_back(std::move(report));
});
}
size_t callback_calls = 0;
attribution_manager_->HandleSource(
SourceBuilder().SetExpiry(kImpressionExpiry).Build(), kFrameId);
attribution_manager_->HandleTrigger(DefaultTrigger(), kFrameId);
attribution_manager_->HandleTrigger(DefaultTrigger(), kFrameId);
std::vector<AttributionReport> reports = StoredReports();
EXPECT_THAT(reports, SizeIs(2));
checkpoint.Call(1);
attribution_manager_->SendReportsForWebUI(
{reports.front().ReportId(), reports.back().ReportId()},
base::BindLambdaForTesting([&]() { callback_calls++; }));
task_environment_.FastForwardBy(base::TimeDelta());
EXPECT_EQ(callback_calls, 0u);
ASSERT_THAT(report_sent_callbacks, SizeIs(2));
ASSERT_THAT(sent_reports, SizeIs(2));
std::move(report_sent_callbacks[0])
.Run(std::move(sent_reports[0]), SendResult(SendResult::Status::kSent));
task_environment_.FastForwardBy(base::TimeDelta());
EXPECT_EQ(callback_calls, 0u);
std::move(report_sent_callbacks[1])
.Run(std::move(sent_reports[1]), SendResult(SendResult::Status::kSent));
task_environment_.FastForwardBy(base::TimeDelta());
EXPECT_EQ(callback_calls, 1u);
}
TEST_F(AttributionManagerImplTest, ExpiredReportsAtStartup_Delayed) {
EXPECT_CALL(*report_sender_, SendReport(_, /*is_debug_report=*/false, _))
.Times(0);
attribution_manager_->HandleSource(
SourceBuilder().SetExpiry(kImpressionExpiry).Build(), kFrameId);
attribution_manager_->HandleTrigger(DefaultTrigger(), kFrameId);
ShutdownManager();
// Fast forward past the expected report time of the first conversion.
task_environment_.FastForwardBy(kFirstReportingWindow +
base::Milliseconds(1));
CreateManager();
// Ensure that the expired report is delayed based on the time the browser
// started and the min and max offline report delays, per
// `AttributionStorageDelegate::GetOfflineReportDelayConfig()`.
base::Time min_new_time = base::Time::Now();
EXPECT_THAT(StoredReports(),
ElementsAre(ReportTimeIs(
AllOf(Ge(min_new_time + kDefaultOfflineReportDelay.min),
Le(min_new_time + kDefaultOfflineReportDelay.max)))));
}
TEST_F(AttributionManagerImplTest,
NonExpiredReportsQueuedAtStartup_NotDelayed) {
EXPECT_CALL(*report_sender_, SendReport(_, /*is_debug_report=*/false, _))
.Times(0);
base::Time start_time = base::Time::Now();
// Create a report that will be reported at t= 2 days.
attribution_manager_->HandleSource(
SourceBuilder().SetExpiry(kImpressionExpiry).Build(), kFrameId);
attribution_manager_->HandleTrigger(DefaultTrigger(), kFrameId);
ShutdownManager();
// Fast forward just before the expected report time.
task_environment_.FastForwardBy(kFirstReportingWindow -
base::Milliseconds(1));
CreateManager();
// Ensure that this report does not receive additional delay.
EXPECT_THAT(StoredReports(),
ElementsAre(ReportTimeIs(start_time + kFirstReportingWindow)));
}
TEST_F(AttributionManagerImplTest, SessionOnlyOrigins_DataDeletedAtShutdown) {
GURL session_only_origin("https://sessiononly.example");
auto impression =
SourceBuilder()
.SetReportingOrigin(*SuitableOrigin::Create(session_only_origin))
.Build();
mock_storage_policy_->AddSessionOnly(session_only_origin);
attribution_manager_->HandleSource(impression, kFrameId);
attribution_manager_->HandleTrigger(
TriggerBuilder()
.SetReportingOrigin(impression.common_info().reporting_origin())
.Build(),
kFrameId);
EXPECT_THAT(StoredSources(), SizeIs(1));
EXPECT_THAT(StoredReports(), SizeIs(1));
ShutdownManager();
CreateManager();
EXPECT_THAT(StoredSources(), IsEmpty());
EXPECT_THAT(StoredReports(), IsEmpty());
}
// Tests that trigger priority cannot result in more than the maximum number of
// reports being sent. A report will never be queued for the expiry window while
// the source is active given we only queue reports which are reported within
// the next 30 minutes, and the expiry window is one hour after expiry time.
// This ensures that a queued report cannot be overwritten by a new, higher
// priority trigger.
TEST_F(AttributionManagerImplTest, ConversionPrioritization_OneReportSent) {
attribution_manager_->HandleSource(
SourceBuilder().SetExpiry(base::Days(7)).Build(), kFrameId);
EXPECT_THAT(StoredSources(), SizeIs(1));
Checkpoint checkpoint;
{
InSequence seq;
EXPECT_CALL(*report_sender_, SendReport(_, /*is_debug_report=*/false, _))
.Times(3)
.WillRepeatedly(InvokeReportSentCallback(SendResult::Status::kSent));
EXPECT_CALL(checkpoint, Call(1));
EXPECT_CALL(*report_sender_, SendReport(_, /*is_debug_report=*/false, _))
.Times(0);
}
attribution_manager_->HandleTrigger(TriggerBuilder().SetPriority(1).Build(),
kFrameId);
attribution_manager_->HandleTrigger(TriggerBuilder().SetPriority(1).Build(),
kFrameId);
attribution_manager_->HandleTrigger(TriggerBuilder().SetPriority(1).Build(),
kFrameId);
EXPECT_THAT(StoredReports(), SizeIs(3));
task_environment_.FastForwardBy(base::Days(7) - base::Minutes(30));
checkpoint.Call(1);
task_environment_.FastForwardBy(base::Minutes(5));
attribution_manager_->HandleTrigger(TriggerBuilder().SetPriority(2).Build(),
kFrameId);
task_environment_.FastForwardBy(base::Hours(1));
}
TEST_F(AttributionManagerImplTest, HandleTrigger_RecordsMetric) {
base::HistogramTester histograms;
attribution_manager_->HandleTrigger(DefaultTrigger(), kFrameId);
EXPECT_THAT(StoredReports(), IsEmpty());
histograms.ExpectUniqueSample(
"Conversions.CreateReportStatus7",
AttributionTrigger::EventLevelResult::kNoMatchingImpressions, 1);
histograms.ExpectUniqueSample(
"Conversions.AggregatableReport.CreateReportStatus3",
AttributionTrigger::AggregatableResult::kNotRegistered, 1);
}
TEST_F(AttributionManagerImplTest, HandleSource_RecordsMetric) {
base::HistogramTester histograms;
attribution_manager_->HandleSource(SourceBuilder().Build(), kFrameId);
task_environment_.RunUntilIdle();
histograms.ExpectUniqueSample("Conversions.SourceStoredStatus2",
StorableSource::Result::kSuccess, 1);
}
TEST_F(AttributionManagerImplTest, OnReportSent_NotifiesObservers) {
base::HistogramTester histograms;
attribution_manager_->HandleSource(SourceBuilder().Build(), kFrameId);
attribution_manager_->HandleTrigger(DefaultTrigger(), kFrameId);
EXPECT_THAT(StoredReports(), SizeIs(1));
MockAttributionObserver observer;
base::ScopedObservation<AttributionManager, AttributionObserver> observation(
&observer);
observation.Observe(attribution_manager_.get());
// Ensure that deleting a report notifies observers.
EXPECT_CALL(observer, OnSourcesChanged).Times(0);
EXPECT_CALL(observer, OnReportsChanged);
EXPECT_CALL(*report_sender_, SendReport(_, /*is_debug_report=*/false, _))
.WillOnce(InvokeReportSentCallback(SendResult::Status::kSent));
task_environment_.FastForwardBy(kFirstReportingWindow);
EXPECT_THAT(StoredReports(), IsEmpty());
}
TEST_F(AttributionManagerImplTest, HandleSource_NotifiesObservers) {
MockAttributionObserver observer;
base::ScopedObservation<AttributionManager, AttributionObserver> observation(
&observer);
observation.Observe(attribution_manager_.get());
SourceBuilder builder;
builder.SetExpiry(kImpressionExpiry).SetSourceEventId(7);
StorableSource source = builder.Build();
Checkpoint checkpoint;
{
InSequence seq;
EXPECT_CALL(observer, OnSourcesChanged);
EXPECT_CALL(observer, OnReportsChanged).Times(0);
EXPECT_CALL(checkpoint, Call(1));
EXPECT_CALL(observer, OnSourcesChanged);
EXPECT_CALL(observer, OnReportsChanged);
EXPECT_CALL(checkpoint, Call(2));
EXPECT_CALL(observer, OnSourcesChanged);
EXPECT_CALL(observer, OnReportsChanged).Times(0);
}
attribution_manager_->HandleSource(source, kFrameId);
EXPECT_THAT(StoredSources(), SizeIs(1));
checkpoint.Call(1);
attribution_manager_->HandleTrigger(DefaultTrigger(), kFrameId);
EXPECT_THAT(StoredReports(), SizeIs(1));
checkpoint.Call(2);
attribution_manager_->HandleSource(
SourceBuilder().SetExpiry(kImpressionExpiry).SetSourceEventId(9).Build(),
kFrameId);
EXPECT_THAT(StoredSources(), SizeIs(2));
}
TEST_F(AttributionManagerImplTest, HandleTrigger_NotifiesObservers) {
MockAttributionObserver observer;
base::ScopedObservation<AttributionManager, AttributionObserver> observation(
&observer);
observation.Observe(attribution_manager_.get());
SourceBuilder builder = TestAggregatableSourceProvider().GetBuilder();
builder.SetExpiry(kImpressionExpiry).SetSourceEventId(7);
StorableSource source = builder.Build();
Checkpoint checkpoint;
{
InSequence seq;
EXPECT_CALL(observer, OnSourcesChanged);
EXPECT_CALL(observer, OnReportsChanged).Times(0);
EXPECT_CALL(checkpoint, Call(1));
// Each stored report should notify sources changed one time.
for (size_t i = 1; i <= 3; i++) {
EXPECT_CALL(observer, OnSourcesChanged);
EXPECT_CALL(observer,
OnReportsChanged(AttributionReport::Type::kEventLevel));
EXPECT_CALL(
observer,
OnReportsChanged(AttributionReport::Type::kAggregatableAttribution));
}
EXPECT_CALL(checkpoint, Call(2));
EXPECT_CALL(observer,
OnReportsChanged(AttributionReport::Type::kEventLevel))
.Times(3);
EXPECT_CALL(
observer,
OnReportsChanged(AttributionReport::Type::kAggregatableAttribution))
.Times(3);
EXPECT_CALL(checkpoint, Call(3));
EXPECT_CALL(observer, OnSourcesChanged);
EXPECT_CALL(observer,
OnReportsChanged(AttributionReport::Type::kEventLevel));
EXPECT_CALL(
observer,
OnReportsChanged(AttributionReport::Type::kAggregatableAttribution))
.Times(0);
}
attribution_manager_->HandleSource(source, kFrameId);
EXPECT_THAT(StoredSources(), SizeIs(1));
checkpoint.Call(1);
// Store the maximum number of reports for the source.
for (size_t i = 1; i <= 3; i++) {
attribution_manager_->HandleTrigger(
DefaultAggregatableTriggerBuilder().Build(), kFrameId);
// i event-level reports and i aggregatable reports.
EXPECT_THAT(StoredReports(), SizeIs(i * 2));
}
checkpoint.Call(2);
// Simulate the reports being sent and removed from storage.
EXPECT_CALL(*aggregation_service_, AssembleReport)
.Times(3)
.WillRepeatedly([](AggregatableReportRequest request,
AggregationService::AssemblyCallback callback) {
std::move(callback).Run(std::move(request),
CreateExampleAggregatableReport(),
AggregationService::AssemblyStatus::kOk);
});
EXPECT_CALL(*report_sender_, SendReport(_, /*is_debug_report=*/false, _))
.Times(6)
.WillRepeatedly(InvokeReportSentCallback(SendResult::Status::kSent));
task_environment_.FastForwardBy(kFirstReportingWindow);
EXPECT_THAT(StoredReports(), IsEmpty());
checkpoint.Call(3);
// The next event-level report should cause the source to reach the
// event-level attribution limit; the report itself shouldn't be stored as
// we've already reached the maximum number of event-level reports per source.
attribution_manager_->HandleTrigger(DefaultTrigger(), kFrameId);
EXPECT_THAT(StoredReports(), IsEmpty());
}
TEST_F(AttributionManagerImplTest, ClearData_NotifiesObservers) {
MockAttributionObserver observer;
base::ScopedObservation<AttributionManager, AttributionObserver> observation(
&observer);
observation.Observe(attribution_manager_.get());
EXPECT_CALL(observer, OnSourcesChanged);
EXPECT_CALL(observer, OnReportsChanged).Times(2);
base::RunLoop run_loop;
attribution_manager_->ClearData(
base::Time::Min(), base::Time::Max(),
base::BindRepeating([](const blink::StorageKey&) { return false; }),
/*filter_builder=*/nullptr,
/*delete_rate_limit_data=*/true, run_loop.QuitClosure());
run_loop.Run();
}
TEST_F(AttributionManagerImplTest,
EmbedderDisallowsImpressions_SourceNotStored) {
base::HistogramTester histograms;
MockAttributionObserver observer;
base::ScopedObservation<AttributionManager, AttributionObserver> observation(
&observer);
observation.Observe(attribution_manager_.get());
const auto source = SourceBuilder().SetExpiry(kImpressionExpiry).Build();
EXPECT_CALL(
observer,
OnSourceHandled(source, testing::Eq(absl::nullopt),
StorableSource::Result::kProhibitedByBrowserPolicy));
MockAttributionReportingContentBrowserClient browser_client;
EXPECT_CALL(
browser_client,
IsAttributionReportingOperationAllowed(
_, ContentBrowserClient::AttributionReportingOperation::kSource, _,
Pointee(url::Origin::Create(GURL("https://impression.test/"))),
IsNull(), Pointee(url::Origin::Create(GURL("https://report.test/")))))
.WillOnce(Return(false));
EXPECT_CALL(browser_client,
IsAttributionReportingOperationAllowed(
_,
ContentBrowserClient::AttributionReportingOperation::
kSourceVerboseDebugReport,
_, _, _, _))
.WillOnce(Return(true));
ScopedContentBrowserClientSetting setting(&browser_client);
attribution_manager_->HandleSource(source, kFrameId);
EXPECT_THAT(StoredSources(), IsEmpty());
histograms.ExpectUniqueSample("Conversions.RegisterImpressionAllowed", false,
1);
}
TEST_F(AttributionManagerImplTest,
EmbedderDisallowsConversions_ReportNotStored) {
base::HistogramTester histograms;
MockAttributionObserver observer;
base::ScopedObservation<AttributionManager, AttributionObserver> observation(
&observer);
observation.Observe(attribution_manager_.get());
const auto trigger = DefaultTrigger();
EXPECT_CALL(observer, OnTriggerHandled(
trigger, _,
AllOf(_,
CreateReportEventLevelStatusIs(
AttributionTrigger::EventLevelResult::
kProhibitedByBrowserPolicy),
CreateReportAggregatableStatusIs(
AttributionTrigger::AggregatableResult::
kProhibitedByBrowserPolicy))));
MockAttributionReportingContentBrowserClient browser_client;
EXPECT_CALL(
browser_client,
IsAttributionReportingOperationAllowed(
_,
AnyOf(ContentBrowserClient::AttributionReportingOperation::kSource,
ContentBrowserClient::AttributionReportingOperation::
kSourceVerboseDebugReport,
ContentBrowserClient::AttributionReportingOperation::
kTriggerVerboseDebugReport),
_, _, _, _))
.WillRepeatedly(Return(true));
EXPECT_CALL(
browser_client,
IsAttributionReportingOperationAllowed(
_, ContentBrowserClient::AttributionReportingOperation::kTrigger, _,
IsNull(),
Pointee(url::Origin::Create(GURL("https://sub.conversion.test/"))),
Pointee(url::Origin::Create(GURL("https://report.test/")))))
.WillOnce(Return(false));
ScopedContentBrowserClientSetting setting(&browser_client);
attribution_manager_->HandleSource(
SourceBuilder().SetExpiry(kImpressionExpiry).Build(), kFrameId);
EXPECT_THAT(StoredSources(), SizeIs(1));
attribution_manager_->HandleTrigger(trigger, kFrameId);
EXPECT_THAT(StoredReports(), IsEmpty());
histograms.ExpectUniqueSample("Conversions.RegisterConversionAllowed", false,
1);
}
TEST_F(AttributionManagerImplTest, EmbedderDisallowsReporting_ReportNotSent) {
MockAttributionReportingContentBrowserClient browser_client;
EXPECT_CALL(
browser_client,
IsAttributionReportingOperationAllowed(
_,
AnyOf(ContentBrowserClient::AttributionReportingOperation::kSource,
ContentBrowserClient::AttributionReportingOperation::kTrigger,
ContentBrowserClient::AttributionReportingOperation::
kSourceVerboseDebugReport,
ContentBrowserClient::AttributionReportingOperation::
kTriggerVerboseDebugReport),
_, _, _, _))
.WillRepeatedly(Return(true));
EXPECT_CALL(
browser_client,
IsAttributionReportingOperationAllowed(
_, ContentBrowserClient::AttributionReportingOperation::kReport, _,
Pointee(url::Origin::Create(GURL("https://impression.test/"))),
Pointee(url::Origin::Create(GURL("https://sub.conversion.test/"))),
Pointee(url::Origin::Create(GURL("https://report.test/")))))
.WillOnce(Return(false));
ScopedContentBrowserClientSetting setting(&browser_client);
base::HistogramTester histograms;
EXPECT_CALL(*report_sender_, SendReport(_, /*is_debug_report=*/false, _))
.Times(0);
attribution_manager_->HandleSource(
SourceBuilder().SetExpiry(kImpressionExpiry).Build(), kFrameId);
attribution_manager_->HandleTrigger(DefaultTrigger(), kFrameId);
EXPECT_THAT(StoredReports(), SizeIs(1));
MockAttributionObserver observer;
base::ScopedObservation<AttributionManager, AttributionObserver> observation(
&observer);
observation.Observe(attribution_manager_.get());
EXPECT_CALL(observer, OnReportSent(_, /*is_debug_report=*/false,
Field(&SendResult::status,
SendResult::Status::kDropped)));
task_environment_.FastForwardBy(kFirstReportingWindow);
EXPECT_THAT(StoredReports(), IsEmpty());
// kDropped = 2.
histograms.ExpectBucketCount("Conversions.ReportSendOutcome3", 2, 1);
}
TEST_F(AttributionManagerImplTest,
EmbedderDisallowsReporting_DebugReportNotSent) {
const auto source_origin = *SuitableOrigin::Deserialize("https://i.test");
const auto destination_origin =
*SuitableOrigin::Deserialize("https://d.test");
const auto reporting_origin = *SuitableOrigin::Deserialize("https://r.test");
cookie_checker_->AddOriginWithDebugCookieSet(reporting_origin);
MockAttributionReportingContentBrowserClient browser_client;
EXPECT_CALL(
browser_client,
IsAttributionReportingOperationAllowed(
_,
AnyOf(ContentBrowserClient::AttributionReportingOperation::kSource,
ContentBrowserClient::AttributionReportingOperation::kTrigger,
ContentBrowserClient::AttributionReportingOperation::
kSourceVerboseDebugReport,
ContentBrowserClient::AttributionReportingOperation::
kTriggerVerboseDebugReport),
_, _, _, _))
.WillRepeatedly(Return(true));
EXPECT_CALL(
browser_client,
IsAttributionReportingOperationAllowed(
_, ContentBrowserClient::AttributionReportingOperation::kReport, _,
Pointee(source_origin), Pointee(destination_origin),
Pointee(reporting_origin)))
.WillOnce(Return(false));
ScopedContentBrowserClientSetting setting(&browser_client);
EXPECT_CALL(*report_sender_, SendReport(_, /*is_debug_report=*/true, _))
.Times(0);
attribution_manager_->HandleSource(
SourceBuilder()
.SetSourceOrigin(source_origin)
.SetDestinationOrigin(destination_origin)
.SetReportingOrigin(reporting_origin)
.SetDebugKey(123)
.SetExpiry(kImpressionExpiry)
.Build(),
kFrameId);
attribution_manager_->HandleTrigger(
TriggerBuilder()
.SetDestinationOrigin(destination_origin)
.SetReportingOrigin(reporting_origin)
.SetDebugKey(456)
.Build(),
kFrameId);
EXPECT_THAT(StoredReports(), SizeIs(1));
}
TEST_F(AttributionManagerImplTest, Offline_NoReportSent) {
Checkpoint checkpoint;
{
InSequence seq;
EXPECT_CALL(*report_sender_, SendReport(_, /*is_debug_report=*/false, _))
.Times(0);
EXPECT_CALL(checkpoint, Call(1));
EXPECT_CALL(*report_sender_, SendReport(_, /*is_debug_report=*/false, _));
}
attribution_manager_->HandleSource(
SourceBuilder().SetExpiry(kImpressionExpiry).Build(), kFrameId);
attribution_manager_->HandleTrigger(DefaultTrigger(), kFrameId);
EXPECT_THAT(StoredReports(), SizeIs(1));
SetConnectionTypeAndWaitForObserversToBeNotified(
network::mojom::ConnectionType::CONNECTION_NONE);
task_environment_.FastForwardBy(kFirstReportingWindow);
checkpoint.Call(1);
SetConnectionTypeAndWaitForObserversToBeNotified(
network::mojom::ConnectionType::CONNECTION_UNKNOWN);
}
class AttributionManagerImplOnlineConnectionTypeTest
: public AttributionManagerImplTest {
protected:
void ConfigureStorageDelegate(
ConfigurableStorageDelegate& delegate) const override {
delegate.set_offline_report_delay_config(
AttributionStorageDelegate::OfflineReportDelayConfig{
.min = base::Minutes(1),
.max = base::Minutes(1),
});
}
};
TEST_F(AttributionManagerImplOnlineConnectionTypeTest,
OnlineConnectionTypeChanges_ReportTimesNotAdjusted) {
attribution_manager_->HandleSource(
SourceBuilder().SetExpiry(kImpressionExpiry).Build(), kFrameId);
attribution_manager_->HandleTrigger(DefaultTrigger(), kFrameId);
EXPECT_THAT(StoredReports(), SizeIs(1));
// Deliberately avoid running tasks so that the connection change and time
// advance can be "atomic", which is necessary because
// `AttributionStorage::AdjustOfflineReportTimes()` only adjusts times for
// reports that should have been sent before now. In other words, the call to
// `AdjustOfflineReportTimes()` would have no effect if we used
// `FastForwardBy()` here, and we wouldn't be able to detect it below.
task_environment_.AdvanceClock(kFirstReportingWindow + base::Microseconds(1));
// This will fail with 0 calls if the report time was adjusted to +1 minute.
EXPECT_CALL(*report_sender_, SendReport(_, /*is_debug_report=*/false, _));
SetConnectionTypeAndWaitForObserversToBeNotified(
network::mojom::ConnectionType::CONNECTION_4G);
// Cause any scheduled tasks to run.
task_environment_.FastForwardBy(base::TimeDelta());
}
TEST_F(AttributionManagerImplTest, TimeFromConversionToReportSendHistogram) {
base::HistogramTester histograms;
attribution_manager_->HandleSource(
SourceBuilder().SetExpiry(kImpressionExpiry).Build(), kFrameId);
attribution_manager_->HandleTrigger(DefaultTrigger(), kFrameId);
ReportSentCallback report_sent_callback;
absl::optional<AttributionReport> sent_report;
EXPECT_CALL(*report_sender_, SendReport(_, /*is_debug_report=*/false, _))
.WillOnce([&](AttributionReport report, bool is_debug_report,
ReportSentCallback callback) {
report_sent_callback = std::move(callback);
sent_report = std::move(report);
});
task_environment_.FastForwardBy(kFirstReportingWindow);
histograms.ExpectUniqueSample("Conversions.TimeFromConversionToReportSend",
kFirstReportingWindow.InHours(), 1);
task_environment_.FastForwardBy(base::Hours(1));
ASSERT_TRUE(report_sent_callback);
ASSERT_TRUE(sent_report);
std::move(report_sent_callback)
.Run(std::move(*sent_report), SendResult(SendResult::Status::kSent));
histograms.ExpectUniqueSample(
"Conversions.TimeFromTriggerToReportSentSuccessfully",
kFirstReportingWindow.InHours() + 1, 1);
}
TEST_F(AttributionManagerImplTest, SendReport_RecordsExtraReportDelay2) {
base::HistogramTester histograms;
Checkpoint checkpoint;
{
InSequence seq;
EXPECT_CALL(*report_sender_, SendReport(_, /*is_debug_report=*/false, _))
.Times(0);
EXPECT_CALL(checkpoint, Call(1));
EXPECT_CALL(*report_sender_, SendReport(_, /*is_debug_report=*/false, _));
}
attribution_manager_->HandleSource(TestAggregatableSourceProvider()
.GetBuilder()
.SetExpiry(kImpressionExpiry)
.Build(),
kFrameId);
attribution_manager_->HandleTrigger(
DefaultAggregatableTriggerBuilder().Build(), kFrameId);
// Prevent the report from being sent until after its original report time.
SetConnectionTypeAndWaitForObserversToBeNotified(
network::mojom::ConnectionType::CONNECTION_NONE);
task_environment_.FastForwardBy(kFirstReportingWindow + base::Days(3));
checkpoint.Call(1);
SetConnectionTypeAndWaitForObserversToBeNotified(
network::mojom::ConnectionType::CONNECTION_UNKNOWN);
task_environment_.FastForwardBy(kDefaultOfflineReportDelay.max);
histograms.ExpectUniqueTimeSample(
"Conversions.ExtraReportDelay2",
base::Days(3) + kDefaultOfflineReportDelay.min, 1);
histograms.ExpectUniqueTimeSample(
"Conversions.AggregatableReport.ExtraReportDelay",
base::Days(3) + kDefaultOfflineReportDelay.min, 1);
}
TEST_F(AttributionManagerImplTest, SendReport_RecordsSchedulerReportDelay) {
base::HistogramTester histograms;
attribution_manager_->HandleSource(TestAggregatableSourceProvider()
.GetBuilder()
.SetExpiry(kImpressionExpiry)
.Build(),
kFrameId);
attribution_manager_->HandleTrigger(
DefaultAggregatableTriggerBuilder().Build(), kFrameId);
EXPECT_THAT(StoredReports(), SizeIs(2));
// Deliberately avoid running tasks so that the scheduler is delayed.
task_environment_.AdvanceClock(kFirstReportingWindow + base::Seconds(1));
// Cause any scheduled tasks to run.
task_environment_.FastForwardBy(base::TimeDelta());
histograms.ExpectUniqueTimeSample("Conversions.SchedulerReportDelay",
base::Seconds(1), 1);
histograms.ExpectUniqueTimeSample(
"Conversions.AggregatableReport.SchedulerReportDelay", base::Seconds(1),
1);
}
TEST_F(AttributionManagerImplTest, SendReportsFromWebUI_DoesNotRecordMetrics) {
base::HistogramTester histograms;
attribution_manager_->HandleSource(
SourceBuilder().SetExpiry(kImpressionExpiry).Build(), kFrameId);
attribution_manager_->HandleTrigger(DefaultTrigger(), kFrameId);
EXPECT_CALL(*report_sender_, SendReport(_, /*is_debug_report=*/false, _));
attribution_manager_->SendReportsForWebUI(
{AttributionReport::EventLevelData::Id(1)}, base::DoNothing());
task_environment_.FastForwardBy(base::TimeDelta());
histograms.ExpectTotalCount("Conversions.ExtraReportDelay2", 0);
histograms.ExpectTotalCount("Conversions.TimeFromConversionToReportSend", 0);
}
class AttributionManagerImplFakeReportTest : public AttributionManagerImplTest {
protected:
void ConfigureStorageDelegate(
ConfigurableStorageDelegate& delegate) const override {
delegate.set_randomized_response(
std::vector<AttributionStorageDelegate::FakeReport>{
{
.trigger_data = 0,
.trigger_time = base::Time::Now() + base::Hours(3),
.report_time = base::Time::Now() + base::Days(1),
},
});
}
};
// Regression test for https://crbug.com/1294519.
TEST_F(AttributionManagerImplFakeReportTest,
FakeReport_UpdatesSendReportTimer) {
Checkpoint checkpoint;
{
InSequence seq;
EXPECT_CALL(*report_sender_, SendReport(_, /*is_debug_report=*/false, _))
.Times(0);
EXPECT_CALL(checkpoint, Call(1));
EXPECT_CALL(*report_sender_, SendReport(_, /*is_debug_report=*/false, _));
}
attribution_manager_->HandleSource(
SourceBuilder().SetExpiry(kImpressionExpiry).Build(), kFrameId);
checkpoint.Call(1);
task_environment_.FastForwardBy(base::Days(1));
}
// Test that multiple source and trigger registrations, with and without debug
// keys present, are handled in the order they are received by the manager.
TEST_F(AttributionManagerImplTest, RegistrationsHandledInOrder) {
cookie_checker_->DeferCallbacks();
const auto r1 = *SuitableOrigin::Deserialize("https://r1.test");
const auto r2 = *SuitableOrigin::Deserialize("https://r2.test");
attribution_manager_->HandleSource(SourceBuilder()
.SetSourceEventId(1)
.SetDebugKey(11)
.SetReportingOrigin(r1)
.SetExpiry(kImpressionExpiry)
.Build(),
kFrameId);
attribution_manager_->HandleTrigger(
TriggerBuilder().SetTriggerData(2).SetReportingOrigin(r1).Build(),
kFrameId);
attribution_manager_->HandleTrigger(TriggerBuilder()
.SetTriggerData(3)
.SetDebugKey(13)
.SetReportingOrigin(r2)
.Build(),
kFrameId);
attribution_manager_->HandleSource(SourceBuilder()
.SetSourceEventId(4)
.SetDebugKey(14)
.SetReportingOrigin(r2)
.SetExpiry(kImpressionExpiry)
.Build(),
kFrameId);
attribution_manager_->HandleTrigger(
TriggerBuilder().SetTriggerData(5).SetReportingOrigin(r2).Build(),
kFrameId);
ASSERT_THAT(StoredSources(), IsEmpty());
ASSERT_THAT(StoredReports(), IsEmpty());
// This should cause the first 2 events to be processed.
cookie_checker_->RunNextDeferredCallback(/*is_debug_cookie_set=*/false);
ASSERT_THAT(StoredSources(), ElementsAre(SourceEventIdIs(1)));
ASSERT_THAT(StoredReports(), ElementsAre(EventLevelDataIs(TriggerDataIs(2))));
// This should cause the next event to be processed. There's no matching
// source, so the trigger should be dropped.
cookie_checker_->RunNextDeferredCallback(/*is_debug_cookie_set=*/false);
ASSERT_THAT(StoredSources(), ElementsAre(SourceEventIdIs(1)));
ASSERT_THAT(StoredReports(), ElementsAre(EventLevelDataIs(TriggerDataIs(2))));
// This should cause the next 2 events to be processed.
cookie_checker_->RunNextDeferredCallback(/*is_debug_cookie_set=*/false);
ASSERT_THAT(StoredSources(),
UnorderedElementsAre(SourceEventIdIs(1), SourceEventIdIs(4)));
ASSERT_THAT(StoredReports(),
UnorderedElementsAre(EventLevelDataIs(TriggerDataIs(2)),
EventLevelDataIs(TriggerDataIs(5))));
}
namespace {
const struct {
const char* name;
absl::optional<uint64_t> input_debug_key;
const char* reporting_origin;
absl::optional<uint64_t> expected_debug_key;
absl::optional<uint64_t> expected_cleared_key;
} kDebugKeyTestCases[] = {
{
"no debug key, no cookie",
absl::nullopt,
"https://r2.test",
absl::nullopt,
absl::nullopt,
},
{
"has debug key, no cookie",
123,
"https://r2.test",
absl::nullopt,
123,
},
{
"no debug key, has cookie",
absl::nullopt,
"https://r1.test",
absl::nullopt,
absl::nullopt,
},
{
"has debug key, has cookie",
123,
"https://r1.test",
123,
absl::nullopt,
},
};
} // namespace
TEST_F(AttributionManagerImplTest, HandleSource_DebugKey) {
cookie_checker_->AddOriginWithDebugCookieSet(
url::Origin::Create(GURL("https://r1.test")));
MockAttributionObserver observer;
base::ScopedObservation<AttributionManager, AttributionObserver> observation(
&observer);
observation.Observe(attribution_manager_.get());
for (const auto& test_case : kDebugKeyTestCases) {
EXPECT_CALL(observer,
OnSourceHandled(_, test_case.expected_cleared_key, _));
attribution_manager_->HandleSource(
SourceBuilder()
.SetReportingOrigin(
*SuitableOrigin::Deserialize(test_case.reporting_origin))
.SetDebugKey(test_case.input_debug_key)
.SetExpiry(kImpressionExpiry)
.Build(),
kFrameId);
EXPECT_THAT(StoredSources(),
ElementsAre(SourceDebugKeyIs(test_case.expected_debug_key)))
<< test_case.name;
attribution_manager_->ClearData(base::Time::Min(), base::Time::Max(),
/*filter=*/base::NullCallback(),
/*filter_builder=*/nullptr,
/*delete_rate_limit_data=*/true,
base::DoNothing());
}
}
TEST_F(AttributionManagerImplTest, HandleTrigger_DebugKey) {
cookie_checker_->AddOriginWithDebugCookieSet(
url::Origin::Create(GURL("https://r1.test")));
MockAttributionObserver observer;
base::ScopedObservation<AttributionManager, AttributionObserver> observation(
&observer);
observation.Observe(attribution_manager_.get());
for (const auto& test_case : kDebugKeyTestCases) {
const auto reporting_origin =
*SuitableOrigin::Deserialize(test_case.reporting_origin);
attribution_manager_->HandleSource(SourceBuilder()
.SetReportingOrigin(reporting_origin)
.SetExpiry(kImpressionExpiry)
.Build(),
kFrameId);
EXPECT_THAT(StoredSources(), SizeIs(1)) << test_case.name;
EXPECT_CALL(observer,
OnTriggerHandled(_, test_case.expected_cleared_key, _));
attribution_manager_->HandleTrigger(
TriggerBuilder()
.SetReportingOrigin(reporting_origin)
.SetDebugKey(test_case.input_debug_key)
.Build(),
kFrameId);
EXPECT_THAT(
StoredReports(),
ElementsAre(AllOf(ReportSourceIs(SourceDebugKeyIs(absl::nullopt)),
TriggerDebugKeyIs(test_case.expected_debug_key))))
<< test_case.name;
attribution_manager_->ClearData(base::Time::Min(), base::Time::Max(),
/*filter=*/base::NullCallback(),
/*filter_builder=*/nullptr,
/*delete_rate_limit_data=*/true,
base::DoNothing());
}
}
TEST_F(AttributionManagerImplTest, DebugReport_SentImmediately) {
const auto reporting_origin = *SuitableOrigin::Deserialize("https://r1.test");
cookie_checker_->AddOriginWithDebugCookieSet(reporting_origin);
const struct {
const char* name;
absl::optional<uint64_t> source_debug_key;
absl::optional<uint64_t> trigger_debug_key;
bool send_expected;
} kTestCases[] = {
{"neither", absl::nullopt, absl::nullopt, false},
{"source", 1, absl::nullopt, false},
{"trigger", absl::nullopt, 1, false},
{"both", 1, 2, true},
};
for (const auto& test_case : kTestCases) {
MockAttributionObserver observer;
base::ScopedObservation<AttributionManager, AttributionObserver>
observation(&observer);
observation.Observe(attribution_manager_.get());
EXPECT_CALL(observer, OnReportSent(_, /*is_debug_report=*/true, _))
.Times(test_case.send_expected * 2);
attribution_manager_->HandleSource(
TestAggregatableSourceProvider()
.GetBuilder()
.SetReportingOrigin(reporting_origin)
.SetExpiry(kImpressionExpiry)
.SetDebugKey(test_case.source_debug_key)
.Build(),
kFrameId);
EXPECT_THAT(StoredSources(), SizeIs(1)) << test_case.name;
if (test_case.send_expected) {
EXPECT_CALL(*aggregation_service_, AssembleReport)
.WillOnce([](AggregatableReportRequest request,
AggregationService::AssemblyCallback callback) {
std::move(callback).Run(std::move(request),
CreateExampleAggregatableReport(),
AggregationService::AssemblyStatus::kOk);
});
} else {
EXPECT_CALL(*aggregation_service_, AssembleReport).Times(0);
}
EXPECT_CALL(*report_sender_, SendReport(_, /*is_debug_report=*/false, _))
.Times(0);
if (test_case.send_expected) {
EXPECT_CALL(
*report_sender_,
SendReport(AllOf(ReportSourceIs(
SourceDebugKeyIs(test_case.source_debug_key)),
TriggerDebugKeyIs(test_case.trigger_debug_key)),
true, _))
.Times(2)
.WillRepeatedly(
InvokeReportSentCallback(SendResult::Status::kTransientFailure));
} else {
EXPECT_CALL(*report_sender_, SendReport(_, /*is_debug_report=*/true, _))
.Times(0);
}
attribution_manager_->HandleTrigger(
DefaultAggregatableTriggerBuilder()
.SetReportingOrigin(reporting_origin)
.SetDebugKey(test_case.trigger_debug_key)
.Build(),
kFrameId);
// one event-level-report, one aggregatable report.
EXPECT_THAT(StoredReports(), SizeIs(2)) << test_case.name;
attribution_manager_->ClearData(base::Time::Min(), base::Time::Max(),
/*filter=*/base::NullCallback(),
/*filter_builder=*/nullptr,
/*delete_rate_limit_data=*/true,
base::DoNothing());
::testing::Mock::VerifyAndClear(&observer);
}
}
TEST_F(AttributionManagerImplTest,
HandleSource_NotifiesObservers_SourceHandled) {
MockAttributionObserver observer;
base::ScopedObservation<AttributionManager, AttributionObserver> observation(
&observer);
observation.Observe(attribution_manager_.get());
const StorableSource source = SourceBuilder().Build();
EXPECT_CALL(observer, OnSourceHandled(source, testing::Eq(absl::nullopt),
StorableSource::Result::kSuccess));
attribution_manager_->HandleSource(source, kFrameId);
EXPECT_THAT(StoredSources(), SizeIs(1));
}
TEST_F(AttributionManagerImplTest,
AggregateReportAssemblySucceeded_ReportSent) {
base::HistogramTester histograms;
attribution_manager_->HandleSource(
TestAggregatableSourceProvider().GetBuilder().Build(), kFrameId);
attribution_manager_->HandleTrigger(
DefaultAggregatableTriggerBuilder().Build(), kFrameId);
MockAttributionObserver observer;
base::ScopedObservation<AttributionManager, AttributionObserver> observation(
&observer);
observation.Observe(attribution_manager_.get());
EXPECT_CALL(
observer,
OnReportSent(ReportTypeIs(AttributionReport::Type::kEventLevel),
/*is_debug_report=*/false,
Field(&SendResult::status, SendResult::Status::kSent)));
EXPECT_CALL(
observer,
OnReportSent(
ReportTypeIs(AttributionReport::Type::kAggregatableAttribution),
/*is_debug_report=*/false,
Field(&SendResult::status, SendResult::Status::kSent)));
Checkpoint checkpoint;
{
InSequence seq;
EXPECT_CALL(*aggregation_service_, AssembleReport).Times(0);
EXPECT_CALL(checkpoint, Call(1));
EXPECT_CALL(*aggregation_service_, AssembleReport)
.WillOnce([](AggregatableReportRequest request,
AggregationService::AssemblyCallback callback) {
std::move(callback).Run(std::move(request),
CreateExampleAggregatableReport(),
AggregationService::AssemblyStatus::kOk);
});
}
// Make sure the report is not sent earlier than its report time.
task_environment_.FastForwardBy(kFirstReportingWindow -
base::Microseconds(1));
checkpoint.Call(1);
std::vector<ReportSentCallback> report_sent_callbacks;
std::vector<AttributionReport> sent_reports;
// One event-level report, one aggregatable report.
EXPECT_CALL(*report_sender_, SendReport(_, /*is_debug_report=*/false, _))
.WillRepeatedly([&](AttributionReport report, bool is_debug_report,
ReportSentCallback callback) {
report_sent_callbacks.push_back(std::move(callback));
sent_reports.push_back(std::move(report));
});
task_environment_.FastForwardBy(base::Microseconds(1));
ASSERT_THAT(report_sent_callbacks, SizeIs(2));
ASSERT_THAT(sent_reports, SizeIs(2));
task_environment_.FastForwardBy(base::Minutes(1));
std::move(report_sent_callbacks[0])
.Run(std::move(sent_reports[0]), SendResult(SendResult::Status::kSent));
std::move(report_sent_callbacks[1])
.Run(std::move(sent_reports[1]), SendResult(SendResult::Status::kSent));
histograms.ExpectUniqueSample(
"Conversions.AggregatableReport.AssembleReportStatus",
AssembleAggregatableReportStatus::kSuccess, 1);
histograms.ExpectUniqueTimeSample(
"Conversions.AggregatableReport.TimeFromTriggerToReportAssembly2",
kFirstReportingWindow, 1);
histograms.ExpectUniqueTimeSample(
"Conversions.AggregatableReport.TimeFromTriggerToReportSentSuccessfully",
kFirstReportingWindow + base::Minutes(1), 1);
// kSent = 0.
histograms.ExpectUniqueSample(
"Conversions.AggregatableReport.ReportSendOutcome2", 0, 1);
}
TEST_F(AttributionManagerImplTest,
AggregateReportAssemblyFailed_ReportNotSent) {
base::HistogramTester histograms;
attribution_manager_->HandleSource(
TestAggregatableSourceProvider().GetBuilder().Build(), kFrameId);
attribution_manager_->HandleTrigger(
DefaultAggregatableTriggerBuilder().Build(), kFrameId);
MockAttributionObserver observer;
base::ScopedObservation<AttributionManager, AttributionObserver> observation(
&observer);
observation.Observe(attribution_manager_.get());
EXPECT_CALL(
observer,
OnReportSent(
ReportTypeIs(AttributionReport::Type::kAggregatableAttribution),
/*is_debug_report=*/false,
Field(&SendResult::status, SendResult::Status::kFailedToAssemble)));
Checkpoint checkpoint;
{
InSequence seq;
EXPECT_CALL(*aggregation_service_, AssembleReport).Times(0);
EXPECT_CALL(checkpoint, Call(1));
EXPECT_CALL(*aggregation_service_, AssembleReport)
.WillOnce([](AggregatableReportRequest request,
AggregationService::AssemblyCallback callback) {
std::move(callback).Run(
std::move(request), absl::nullopt,
AggregationService::AssemblyStatus::kAssemblyFailed);
});
}
// Make sure the report is not sent earlier than its report time.
task_environment_.FastForwardBy(kFirstReportingWindow -
base::Microseconds(1));
checkpoint.Call(1);
// Event-level report was sent.
EXPECT_CALL(*report_sender_, SendReport(_, /*is_debug_report=*/false, _));
task_environment_.FastForwardBy(base::Microseconds(1));
histograms.ExpectUniqueSample(
"Conversions.AggregatableReport.AssembleReportStatus",
AssembleAggregatableReportStatus::kAssembleReportFailed, 1);
histograms.ExpectUniqueTimeSample(
"Conversions.AggregatableReport.TimeFromTriggerToReportAssembly2",
kFirstReportingWindow, 1);
histograms.ExpectTotalCount(
"Conversions.AggregatableReport.TimeFromTriggerToReportSentSuccessfully",
0);
// kFailedToAssemble = 3.
histograms.ExpectUniqueSample(
"Conversions.AggregatableReport.ReportSendOutcome2", 3, 1);
}
TEST_F(AttributionManagerImplTest, AggregationServiceDisabled_ReportNotSent) {
base::HistogramTester histograms;
ShutdownAggregationService();
attribution_manager_->HandleSource(
TestAggregatableSourceProvider().GetBuilder().Build(), kFrameId);
attribution_manager_->HandleTrigger(
DefaultAggregatableTriggerBuilder().Build(), kFrameId);
// Event-level report was sent.
EXPECT_CALL(*report_sender_, SendReport(_, /*is_debug_report=*/false, _));
task_environment_.FastForwardBy(kFirstReportingWindow);
histograms.ExpectUniqueSample(
"Conversions.AggregatableReport.AssembleReportStatus",
AssembleAggregatableReportStatus::kAggregationServiceUnavailable, 1);
histograms.ExpectUniqueTimeSample(
"Conversions.AggregatableReport.TimeFromTriggerToReportAssembly2",
kFirstReportingWindow, 1);
histograms.ExpectTotalCount(
"Conversions.AggregatableReport.TimeFromTriggerToReportSentSuccessfully",
0);
// kFailedToAssemble = 3.
histograms.ExpectUniqueSample(
"Conversions.AggregatableReport.ReportSendOutcome2", 3, 1);
}
TEST_F(AttributionManagerImplTest, GetFailedReportDelay) {
const struct {
int failed_send_attempts;
absl::optional<base::TimeDelta> expected;
} kTestCases[] = {
{1, base::Minutes(5)},
{2, base::Minutes(15)},
{3, absl::nullopt},
};
for (const auto& test_case : kTestCases) {
EXPECT_EQ(test_case.expected,
GetFailedReportDelay(test_case.failed_send_attempts))
<< "failed_send_attempts=" << test_case.failed_send_attempts;
}
}
TEST_F(AttributionManagerImplTest, TooManyEventsInQueue) {
base::HistogramTester histograms;
// Prevent sources from being removed from the queue.
cookie_checker_->DeferCallbacks();
for (size_t i = 0; i <= kMaxPendingEvents; i++) {
attribution_manager_->HandleSource(
SourceBuilder().SetDebugKey(i).SetExpiry(kImpressionExpiry).Build(),
kFrameId);
}
histograms.ExpectBucketCount("Conversions.EnqueueEventAllowed", true,
kMaxPendingEvents);
histograms.ExpectBucketCount("Conversions.EnqueueEventAllowed", false, 1);
// Unblock the cookie checks. Only the first `kMaxPendingEvents` sources
// should be stored.
for (size_t i = 0; i <= kMaxPendingEvents; i++) {
cookie_checker_->RunNextDeferredCallback(/*is_debug_cookie_set=*/true);
}
std::vector<StoredSource> sources = StoredSources();
ASSERT_THAT(sources, SizeIs(kMaxPendingEvents));
for (size_t i = 0; i < kMaxPendingEvents; i++) {
EXPECT_THAT(sources[i], SourceDebugKeyIs(i));
}
}
TEST_F(AttributionManagerImplTest, TriggerVerboseDebugReport_ReportSent) {
const auto reporting_origin = *SuitableOrigin::Deserialize("https://r1.test");
cookie_checker_->AddOriginWithDebugCookieSet(reporting_origin);
Checkpoint checkpoint;
{
InSequence seq;
EXPECT_CALL(*report_sender_, SendReport(_, _)).Times(0);
EXPECT_CALL(checkpoint, Call(1));
EXPECT_CALL(*report_sender_, SendReport(_, _));
}
// Failed without debug reporting.
attribution_manager_->HandleTrigger(
TriggerBuilder().SetReportingOrigin(reporting_origin).Build(), kFrameId);
task_environment_.RunUntilIdle();
// Trigger registered within a fenced frame failed with debug reporting, but
// no debug report is sent.
attribution_manager_->HandleTrigger(TriggerBuilder()
.SetReportingOrigin(reporting_origin)
.SetDebugReporting(true)
.SetIsWithinFencedFrame(true)
.Build(),
kFrameId);
task_environment_.RunUntilIdle();
// Trigger registered outside a fenced frame tree failed with debug reporting
// but no debug cookie is set, therefore no debug report is sent.
attribution_manager_->HandleTrigger(
TriggerBuilder()
.SetReportingOrigin(*SuitableOrigin::Deserialize("https://r2.test"))
.SetDebugReporting(true)
.Build(),
kFrameId);
task_environment_.RunUntilIdle();
{
// Trigger registered outside a fenced frame tree failed with debug
// reporting and debug cookie is set, but feature off.
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndDisableFeature(
kAttributionVerboseDebugReporting);
attribution_manager_->HandleTrigger(
TriggerBuilder()
.SetReportingOrigin(reporting_origin)
.SetDebugReporting(true)
.Build(),
kFrameId);
task_environment_.RunUntilIdle();
}
checkpoint.Call(1);
{
// Trigger registered outside a fenced frame tree failed with debug
// reporting and debug cookie is set, and feature on.
base::test::ScopedFeatureList scoped_feature_list{
kAttributionVerboseDebugReporting};
attribution_manager_->HandleTrigger(
TriggerBuilder()
.SetReportingOrigin(reporting_origin)
.SetDebugReporting(true)
.Build(),
kFrameId);
task_environment_.RunUntilIdle();
}
}
TEST_F(AttributionManagerImplTest,
EmbedderDisallowsTriggerVerboseDebugReport_NoReportSent) {
const auto reporting_origin = *SuitableOrigin::Deserialize("https://r1.test");
cookie_checker_->AddOriginWithDebugCookieSet(reporting_origin);
EXPECT_CALL(*report_sender_, SendReport(_, _)).Times(0);
MockAttributionReportingContentBrowserClient browser_client;
EXPECT_CALL(
browser_client,
IsAttributionReportingOperationAllowed(
_, ContentBrowserClient::AttributionReportingOperation::kTrigger, _,
_, _, _))
.WillOnce(Return(true));
EXPECT_CALL(
browser_client,
IsAttributionReportingOperationAllowed(
_,
ContentBrowserClient::AttributionReportingOperation::
kTriggerVerboseDebugReport,
_, IsNull(),
Pointee(url::Origin::Create(GURL("https://sub.conversion.test/"))),
Pointee(reporting_origin)))
.WillOnce(Return(false));
ScopedContentBrowserClientSetting setting(&browser_client);
attribution_manager_->HandleTrigger(TriggerBuilder()
.SetReportingOrigin(reporting_origin)
.SetDebugReporting(true)
.Build(),
kFrameId);
task_environment_.RunUntilIdle();
}
class AttributionManagerImplDebugReportTest
: public AttributionManagerImplTest {
protected:
void ConfigureStorageDelegate(
ConfigurableStorageDelegate& delegate) const override {
delegate.set_max_destinations_per_source_site_reporting_origin(1);
}
};
TEST_F(AttributionManagerImplDebugReportTest, VerboseDebugReport_ReportSent) {
absl::optional<AttributionDebugReport> sent_report;
Checkpoint checkpoint;
{
InSequence seq;
EXPECT_CALL(*report_sender_, SendReport(_, _)).Times(0);
EXPECT_CALL(checkpoint, Call(1));
EXPECT_CALL(*report_sender_, SendReport(_, _))
.WillOnce([&](AttributionDebugReport report,
DebugReportSentCallback callback) {
sent_report = std::move(report);
});
}
attribution_manager_->HandleSource(SourceBuilder().Build(), kFrameId);
const auto destination_origin =
*SuitableOrigin::Deserialize("https://d.test");
// Failed without debug reporting.
attribution_manager_->HandleSource(
SourceBuilder().SetDestinationOrigin(destination_origin).Build(),
kFrameId);
task_environment_.RunUntilIdle();
EXPECT_THAT(StoredSources(), SizeIs(1));
// Source registered within a fenced frame failed with debug reporting, but
// no debug report is sent.
attribution_manager_->HandleSource(
SourceBuilder()
.SetDestinationOrigin(destination_origin)
.SetIsWithinFencedFrame(true)
.SetDebugReporting(true)
.Build(),
kFrameId);
task_environment_.RunUntilIdle();
EXPECT_THAT(StoredSources(), SizeIs(1));
{
// Source registered outside a fenced frame failed with debug reporting, but
// feature off.
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndDisableFeature(
kAttributionVerboseDebugReporting);
attribution_manager_->HandleSource(
SourceBuilder()
.SetDestinationOrigin(destination_origin)
.SetDebugReporting(true)
.Build(),
kFrameId);
task_environment_.RunUntilIdle();
EXPECT_THAT(StoredSources(), SizeIs(1));
}
checkpoint.Call(1);
{
// Source registered outside a fenced frame failed with debug reporting, and
// feature on.
base::test::ScopedFeatureList scoped_feature_list{
kAttributionVerboseDebugReporting};
attribution_manager_->HandleSource(
SourceBuilder()
.SetDestinationOrigin(destination_origin)
.SetDebugReporting(true)
.Build(),
kFrameId);
task_environment_.RunUntilIdle();
EXPECT_THAT(StoredSources(), SizeIs(1));
ASSERT_TRUE(sent_report);
const base::Value::List& report_body = sent_report->ReportBody();
ASSERT_EQ(report_body.size(), 1u);
ASSERT_TRUE(report_body.front().is_dict());
const base::Value::Dict* report_data =
report_body.front().GetDict().FindDict("body");
ASSERT_TRUE(report_data);
EXPECT_TRUE(report_data->Find("source_site"));
}
}
TEST_F(AttributionManagerImplDebugReportTest,
EmbedderDisallowsSourceVerboseDebugReport_NoReportSent) {
MockAttributionReportingContentBrowserClient browser_client;
EXPECT_CALL(
browser_client,
IsAttributionReportingOperationAllowed(
_, ContentBrowserClient::AttributionReportingOperation::kSource, _, _,
_, _))
.WillRepeatedly(Return(true));
EXPECT_CALL(
browser_client,
IsAttributionReportingOperationAllowed(
_,
ContentBrowserClient::AttributionReportingOperation::
kSourceVerboseDebugReport,
_, Pointee(url::Origin::Create(GURL("https://impression.test/"))),
IsNull(), Pointee(url::Origin::Create(GURL("https://report.test/")))))
.WillRepeatedly(Return(false));
ScopedContentBrowserClientSetting setting(&browser_client);
EXPECT_CALL(*report_sender_, SendReport(_, _)).Times(0);
attribution_manager_->HandleSource(SourceBuilder().Build(), kFrameId);
attribution_manager_->HandleSource(
SourceBuilder()
.SetDestinationOrigin(*SuitableOrigin::Deserialize("https://d.test"))
.SetDebugReporting(true)
.Build(),
kFrameId);
task_environment_.RunUntilIdle();
EXPECT_THAT(StoredSources(), SizeIs(1));
}
class AttributionManagerImplCookieBasedDebugReportTest
: public AttributionManagerImplTest {
protected:
void ConfigureStorageDelegate(
ConfigurableStorageDelegate& delegate) const override {
delegate.set_max_sources_per_origin(1);
}
};
TEST_F(AttributionManagerImplCookieBasedDebugReportTest,
VerboseDebugReport_ReportSent) {
MockAttributionObserver observer;
base::ScopedObservation<AttributionManager, AttributionObserver> observation(
&observer);
observation.Observe(attribution_manager_.get());
const int kExpectedStatus = 200;
EXPECT_CALL(observer, OnDebugReportSent(_, kExpectedStatus, _));
const auto reporting_origin = *SuitableOrigin::Deserialize("https://r1.test");
cookie_checker_->AddOriginWithDebugCookieSet(reporting_origin);
Checkpoint checkpoint;
{
InSequence seq;
EXPECT_CALL(*report_sender_, SendReport(_, _)).Times(0);
EXPECT_CALL(checkpoint, Call(1));
EXPECT_CALL(*report_sender_, SendReport(_, _))
.WillOnce([](AttributionDebugReport report,
DebugReportSentCallback callback) {
std::move(callback).Run(std::move(report), kExpectedStatus);
});
}
attribution_manager_->HandleSource(SourceBuilder().Build(), kFrameId);
// Failed without debug reporting.
attribution_manager_->HandleSource(
SourceBuilder().SetReportingOrigin(reporting_origin).Build(), kFrameId);
task_environment_.RunUntilIdle();
// Source registered within a fenced frame failed with debug reporting, but
// no debug report is sent.
attribution_manager_->HandleSource(SourceBuilder()
.SetReportingOrigin(reporting_origin)
.SetDebugReporting(true)
.SetIsWithinFencedFrame(true)
.Build(),
kFrameId);
task_environment_.RunUntilIdle();
// Source registered outside a fenced frame failed with debug reporting but no
// debug cookie is set, therefore no debug report is sent.
attribution_manager_->HandleSource(
SourceBuilder()
.SetReportingOrigin(*SuitableOrigin::Deserialize("https://r2.test"))
.SetDebugReporting(true)
.Build(),
kFrameId);
task_environment_.RunUntilIdle();
checkpoint.Call(1);
// Source registered outside a fenced frame with debug reporting and debug
// cookie is set.
attribution_manager_->HandleSource(SourceBuilder()
.SetReportingOrigin(reporting_origin)
.SetDebugReporting(true)
.Build(),
kFrameId);
task_environment_.RunUntilIdle();
}
} // namespace content