blob: 2afea6f0747d4d117c356d441f5acc4797db10dc [file] [log] [blame]
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/attribution_reporting/attribution_manager_impl.h"
#include <initializer_list>
#include <memory>
#include <utility>
#include <vector>
#include "base/bind.h"
#include "base/callback_helpers.h"
#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/guid.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/sequenced_task_runner.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/mock_callback.h"
#include "base/time/time.h"
#include "base/values.h"
#include "build/build_config.h"
#include "content/browser/aggregation_service/aggregatable_report.h"
#include "content/browser/aggregation_service/aggregation_service_impl.h"
#include "content/browser/aggregation_service/aggregation_service_test_utils.h"
#include "content/browser/attribution_reporting/aggregatable_attribution.h"
#include "content/browser/attribution_reporting/attribution_cookie_checker.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/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/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"
namespace content {
namespace {
using ::testing::_;
using ::testing::AllOf;
using ::testing::ElementsAre;
using ::testing::Expectation;
using ::testing::Field;
using ::testing::Ge;
using ::testing::InSequence;
using ::testing::IsEmpty;
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)>;
class MockAttributionObserver : public AttributionObserver {
public:
MOCK_METHOD(void, OnSourcesChanged, (), (override));
MOCK_METHOD(void, OnReportsChanged, (), (override));
MOCK_METHOD(void,
OnSourceHandled,
(const StorableSource& source, StorableSource::Result result),
(override));
MOCK_METHOD(void,
OnSourceDeactivated,
(const DeactivatedSource& source),
(override));
MOCK_METHOD(void,
OnReportSent,
(const AttributionReport& report, const SendResult& info),
(override));
MOCK_METHOD(void,
OnTriggerHandled,
(const CreateReportResult& result),
(override));
};
// 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:
// AttributionReportSender:
void SendReport(AttributionReport report,
ReportSentCallback callback) override {
calls_.push_back(report);
callbacks_.emplace_back(std::move(report), std::move(callback));
}
const std::vector<AttributionReport>& calls() const { return calls_; }
void RunCallback(size_t index, SendResult::Status status) {
std::move(callbacks_[index].second)
.Run(std::move(callbacks_[index].first),
SendResult(status, /*http_response_code=*/0));
}
void Reset() {
calls_.clear();
callbacks_.clear();
}
void RunCallbacksAndReset(
std::initializer_list<SendResult::Status> statuses) {
ASSERT_THAT(callbacks_, SizeIs(statuses.size()));
const auto* status_it = statuses.begin();
for (auto& callback : callbacks_) {
std::move(callback.second)
.Run(std::move(callback.first),
SendResult(*status_it, /*http_response_code=*/0));
status_it++;
}
Reset();
}
private:
std::vector<AttributionReport> calls_;
std::vector<std::pair<AttributionReport, ReportSentCallback>> callbacks_;
};
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_;
};
class MockAggregationService : public AggregationServiceImpl {
public:
explicit MockAggregationService(StoragePartitionImpl* partition)
: AggregationServiceImpl(/*run_in_memory=*/true,
/*user_data_directory=*/base::FilePath(),
partition) {}
void AssembleReport(AggregatableReportRequest report_request,
AssemblyCallback callback) override {
calls_.push_back(std::move(report_request));
callbacks_.push_back(std::move(callback));
}
using AssembleReportCalls = std::vector<AggregatableReportRequest>;
const AssembleReportCalls& calls() const { return calls_; }
void RunCallback(size_t index,
absl::optional<AggregatableReport> report,
AssemblyStatus status) {
std::move(callbacks_[index]).Run(std::move(report), status);
}
void Reset() {
calls_.clear();
callbacks_.clear();
}
private:
AssembleReportCalls calls_;
std::vector<AssemblyCallback> 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>()),
cookie_checker_(new MockCookieChecker()),
report_sender_(new MockReportSender()) {
EXPECT_TRUE(dir_.CreateUniqueTempDir());
content::SetNetworkConnectionTrackerForTesting(
network::TestNetworkConnectionTracker::GetInstance());
CreateManager();
CreateAggregationService();
}
void CreateManager() {
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(
AttributionStorageDelegate::OfflineReportDelayConfig{
.min = base::Minutes(0),
.max = base::Minutes(1),
});
storage_delegate_ = storage_delegate.get();
attribution_manager_ = AttributionManagerImpl::CreateForTesting(
AttributionManagerImpl::DefaultIsReportAllowedCallback(
browser_context_.get()),
dir_.GetPath(), mock_storage_policy_, std::move(storage_delegate),
absl::WrapUnique(cookie_checker_.get()),
absl::WrapUnique(report_sender_.get()),
static_cast<StoragePartitionImpl*>(
browser_context_->GetDefaultStoragePartition()));
}
void ShutdownManager() {
// Allow the network sender to be reused across `CreateManager()`
// invocations by ensuring that the manager doesn't destroy it.
if (attribution_manager_) {
attribution_manager_->cookie_checker_.release();
attribution_manager_->report_sender_.release();
attribution_manager_.reset();
}
}
void CreateAggregationService() {
auto* partition = static_cast<StoragePartitionImpl*>(
browser_context_->GetDefaultStoragePartition());
auto aggregation_service =
std::make_unique<MockAggregationService>(partition);
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() {
std::vector<AttributionReport> result;
base::RunLoop loop;
attribution_manager_->GetPendingReportsForInternalUse(
base::BindLambdaForTesting([&](std::vector<AttributionReport> reports) {
result = std::move(reports);
loop.Quit();
}));
loop.Run();
return result;
}
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:
base::ScopedTempDir dir_;
BrowserTaskEnvironment task_environment_;
std::unique_ptr<TestBrowserContext> browser_context_;
scoped_refptr<storage::MockSpecialStoragePolicy> mock_storage_policy_;
const raw_ptr<MockCookieChecker> cookie_checker_;
const raw_ptr<MockReportSender> report_sender_;
raw_ptr<ConfigurableStorageDelegate> storage_delegate_;
raw_ptr<MockAggregationService> aggregation_service_;
std::unique_ptr<AttributionManagerImpl> attribution_manager_;
};
TEST_F(AttributionManagerImplTest, ImpressionRegistered_ReturnedToWebUI) {
auto impression = SourceBuilder()
.SetExpiry(kImpressionExpiry)
.SetSourceEventId(100)
.Build();
attribution_manager_->HandleSource(impression);
EXPECT_THAT(StoredSources(),
ElementsAre(CommonSourceInfoIs(impression.common_info())));
}
TEST_F(AttributionManagerImplTest, ExpiredImpression_NotReturnedToWebUI) {
attribution_manager_->HandleSource(SourceBuilder()
.SetExpiry(kImpressionExpiry)
.SetSourceEventId(100)
.Build());
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());
auto conversion = DefaultTrigger();
attribution_manager_->HandleTrigger(conversion);
AttributionReport expected_report =
ReportBuilder(AttributionInfoBuilder(builder.BuildStored())
.SetTime(base::Time::Now())
.Build())
.SetTriggerData(conversion.trigger_data())
.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) {
attribution_manager_->HandleSource(
SourceBuilder().SetExpiry(kImpressionExpiry).Build());
attribution_manager_->HandleTrigger(DefaultTrigger());
// Make sure the report is not sent earlier than its report time.
task_environment_.FastForwardBy(kFirstReportingWindow -
base::Microseconds(1));
EXPECT_THAT(report_sender_->calls(), IsEmpty());
task_environment_.FastForwardBy(base::Microseconds(1));
EXPECT_THAT(report_sender_->calls(), SizeIs(1));
}
TEST_F(AttributionManagerImplTest,
MultipleReportsWithSameReportTime_AllSentSimultaneously) {
const GURL url_a(
"https://a.example/.well-known/attribution-reporting/report-attribution");
const GURL url_b(
"https://b.example/.well-known/attribution-reporting/report-attribution");
const GURL url_c(
"https://c.example/.well-known/attribution-reporting/report-attribution");
const auto origin_a = url::Origin::Create(url_a);
const auto origin_b = url::Origin::Create(url_b);
const auto origin_c = url::Origin::Create(url_c);
attribution_manager_->HandleSource(SourceBuilder()
.SetExpiry(kImpressionExpiry)
.SetReportingOrigin(origin_a)
.Build());
attribution_manager_->HandleTrigger(
TriggerBuilder().SetReportingOrigin(origin_a).Build());
attribution_manager_->HandleSource(SourceBuilder()
.SetExpiry(kImpressionExpiry)
.SetReportingOrigin(origin_b)
.Build());
attribution_manager_->HandleTrigger(
TriggerBuilder().SetReportingOrigin(origin_b).Build());
attribution_manager_->HandleSource(SourceBuilder()
.SetExpiry(kImpressionExpiry)
.SetReportingOrigin(origin_c)
.Build());
attribution_manager_->HandleTrigger(
TriggerBuilder().SetReportingOrigin(origin_c).Build());
EXPECT_THAT(StoredReports(), SizeIs(3));
// Make sure the reports are not sent earlier than their report time.
task_environment_.FastForwardBy(kFirstReportingWindow -
base::Microseconds(1));
EXPECT_THAT(report_sender_->calls(), IsEmpty());
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(report_sender_->calls(),
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-attribution");
const GURL url_b(
"https://b.example/.well-known/attribution-reporting/report-attribution");
const auto origin_a = url::Origin::Create(url_a);
const auto origin_b = url::Origin::Create(url_b);
attribution_manager_->HandleSource(SourceBuilder()
.SetExpiry(kImpressionExpiry)
.SetReportingOrigin(origin_a)
.Build());
attribution_manager_->HandleTrigger(
TriggerBuilder().SetReportingOrigin(origin_a).Build());
task_environment_.FastForwardBy(base::Microseconds(1));
attribution_manager_->HandleSource(SourceBuilder()
.SetExpiry(kImpressionExpiry)
.SetReportingOrigin(origin_b)
.Build());
attribution_manager_->HandleTrigger(
TriggerBuilder().SetReportingOrigin(origin_b).Build());
EXPECT_THAT(StoredReports(), SizeIs(2));
// Make sure the reports are not sent earlier than their report time.
task_environment_.FastForwardBy(kFirstReportingWindow -
base::Microseconds(2));
EXPECT_THAT(report_sender_->calls(), IsEmpty());
task_environment_.FastForwardBy(base::Microseconds(1));
EXPECT_THAT(report_sender_->calls(), ElementsAre(ReportURLIs(url_a)));
report_sender_->Reset();
task_environment_.FastForwardBy(base::Microseconds(1));
EXPECT_THAT(report_sender_->calls(), ElementsAre(ReportURLIs(url_b)));
}
TEST_F(AttributionManagerImplTest, SenderStillHandlingReport_NotSentAgain) {
attribution_manager_->HandleSource(
SourceBuilder().SetExpiry(kImpressionExpiry).Build());
attribution_manager_->HandleTrigger(DefaultTrigger());
task_environment_.FastForwardBy(kFirstReportingWindow);
EXPECT_THAT(report_sender_->calls(), SizeIs(1));
report_sender_->Reset();
ForceGetReportsToSend();
// The sender hasn't invoked the callback, so the manager shouldn't try to
// send the report again.
EXPECT_THAT(report_sender_->calls(), IsEmpty());
}
TEST_F(AttributionManagerImplTest,
QueuedReportFailedWithShouldRetry_QueuedAgain) {
base::HistogramTester histograms;
attribution_manager_->HandleSource(
SourceBuilder().SetExpiry(kImpressionExpiry).Build());
attribution_manager_->HandleTrigger(DefaultTrigger());
task_environment_.FastForwardBy(kFirstReportingWindow);
EXPECT_THAT(report_sender_->calls(), SizeIs(1));
report_sender_->RunCallbacksAndReset({SendResult::Status::kTransientFailure});
// First report delay.
task_environment_.FastForwardBy(base::Minutes(5));
EXPECT_THAT(report_sender_->calls(), SizeIs(1));
report_sender_->RunCallbacksAndReset({SendResult::Status::kTransientFailure});
// Second report delay.
task_environment_.FastForwardBy(base::Minutes(15));
EXPECT_THAT(report_sender_->calls(), SizeIs(1));
report_sender_->RunCallbacksAndReset({SendResult::Status::kTransientFailure});
// kFailed = 1.
histograms.ExpectUniqueSample("Conversions.ReportSendOutcome", 1, 1);
}
TEST_F(AttributionManagerImplTest, RetryLogicOverridesGetReportTimer) {
const GURL url_a(
"https://a.example/.well-known/attribution-reporting/report-attribution");
const GURL url_b(
"https://b.example/.well-known/attribution-reporting/report-attribution");
const auto origin_a = url::Origin::Create(url_a);
const auto origin_b = url::Origin::Create(url_b);
attribution_manager_->HandleSource(SourceBuilder()
.SetExpiry(kImpressionExpiry)
.SetReportingOrigin(origin_a)
.Build());
attribution_manager_->HandleTrigger(
TriggerBuilder().SetReportingOrigin(origin_a).Build());
task_environment_.FastForwardBy(base::Minutes(10));
attribution_manager_->HandleSource(SourceBuilder()
.SetExpiry(kImpressionExpiry)
.SetReportingOrigin(origin_b)
.Build());
attribution_manager_->HandleTrigger(
TriggerBuilder().SetReportingOrigin(origin_b).Build());
EXPECT_THAT(StoredReports(), SizeIs(2));
task_environment_.FastForwardBy(kFirstReportingWindow - base::Minutes(10));
EXPECT_THAT(report_sender_->calls(), ElementsAre(ReportURLIs(url_a)));
// 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.
report_sender_->RunCallbacksAndReset({SendResult::Status::kTransientFailure});
task_environment_.FastForwardBy(base::Minutes(5));
EXPECT_THAT(report_sender_->calls(), ElementsAre(ReportURLIs(url_a)));
}
TEST_F(AttributionManagerImplTest,
QueuedReportFailedWithoutShouldRetry_NotQueuedAgain) {
base::HistogramTester histograms;
attribution_manager_->HandleSource(
SourceBuilder().SetExpiry(kImpressionExpiry).Build());
attribution_manager_->HandleTrigger(DefaultTrigger());
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);
task_environment_.FastForwardBy(kFirstReportingWindow);
EXPECT_THAT(report_sender_->calls(), SizeIs(1));
report_sender_->RunCallbacksAndReset({SendResult::Status::kFailure});
EXPECT_THAT(StoredReports(), IsEmpty());
// kFailed = 1.
histograms.ExpectUniqueSample("Conversions.ReportSendOutcome", 1, 1);
}
TEST_F(AttributionManagerImplTest, QueuedReportAlwaysFails_StopsSending) {
base::HistogramTester histograms;
attribution_manager_->HandleSource(
SourceBuilder().SetExpiry(kImpressionExpiry).Build());
attribution_manager_->HandleTrigger(DefaultTrigger());
task_environment_.FastForwardBy(kFirstReportingWindow -
base::Milliseconds(1));
EXPECT_THAT(report_sender_->calls(), IsEmpty());
// The report is sent at its expected report time.
task_environment_.FastForwardBy(base::Milliseconds(1));
EXPECT_THAT(report_sender_->calls(), SizeIs(1));
report_sender_->RunCallbacksAndReset({SendResult::Status::kTransientFailure});
// The report is sent at the first retry time of +5 minutes.
task_environment_.FastForwardBy(base::Minutes(5));
EXPECT_THAT(report_sender_->calls(), SizeIs(1));
report_sender_->RunCallbacksAndReset({SendResult::Status::kTransientFailure});
// The report is sent at the second retry time of +15 minutes.
task_environment_.FastForwardBy(base::Minutes(15));
EXPECT_THAT(report_sender_->calls(), SizeIs(1));
report_sender_->RunCallbacksAndReset({SendResult::Status::kTransientFailure});
// 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.ReportSendOutcome", 1, 1);
}
TEST_F(AttributionManagerImplTest, ReportExpiredAtStartup_Sent) {
attribution_manager_->HandleSource(
SourceBuilder().SetExpiry(kImpressionExpiry).Build());
attribution_manager_->HandleTrigger(DefaultTrigger());
ShutdownManager();
// Fast-forward past the reporting window and past report expiry.
task_environment_.FastForwardBy(kFirstReportingWindow);
task_environment_.FastForwardBy(base::Days(100));
EXPECT_THAT(report_sender_->calls(), IsEmpty());
// Simulate startup and ensure the report is sent before being expired.
// Advance by the max offline report delay, per
// `AttributionStorageDelegate::GetOfflineReportDelayConfig()`.
CreateManager();
task_environment_.FastForwardBy(
storage_delegate_->GetOfflineReportDelayConfig()->max);
EXPECT_THAT(report_sender_->calls(), SizeIs(1));
}
TEST_F(AttributionManagerImplTest, ReportSent_Deleted) {
base::HistogramTester histograms;
attribution_manager_->HandleSource(
SourceBuilder().SetExpiry(kImpressionExpiry).Build());
attribution_manager_->HandleTrigger(DefaultTrigger());
task_environment_.FastForwardBy(kFirstReportingWindow);
EXPECT_THAT(report_sender_->calls(), SizeIs(1));
report_sender_->RunCallbacksAndReset({SendResult::Status::kSent});
EXPECT_THAT(StoredReports(), IsEmpty());
EXPECT_THAT(report_sender_->calls(), IsEmpty());
// kSent = 0.
histograms.ExpectUniqueSample("Conversions.ReportSendOutcome", 0, 1);
}
TEST_F(AttributionManagerImplTest, QueuedReportSent_ObserversNotified) {
base::HistogramTester histograms;
MockAttributionObserver observer;
base::ScopedObservation<AttributionManager, AttributionObserver> observation(
&observer);
observation.Observe(attribution_manager_.get());
EXPECT_CALL(observer, OnReportSent(ReportSourceIs(SourceEventIdIs(1u)), _));
EXPECT_CALL(observer, OnReportSent(ReportSourceIs(SourceEventIdIs(2u)), _));
EXPECT_CALL(observer, OnReportSent(ReportSourceIs(SourceEventIdIs(3u)), _));
attribution_manager_->HandleSource(
SourceBuilder().SetSourceEventId(1).SetExpiry(kImpressionExpiry).Build());
attribution_manager_->HandleTrigger(DefaultTrigger());
task_environment_.FastForwardBy(kFirstReportingWindow);
// This one should be stored, as its status is `kDropped`.
attribution_manager_->HandleSource(
SourceBuilder().SetSourceEventId(2).SetExpiry(kImpressionExpiry).Build());
attribution_manager_->HandleTrigger(DefaultTrigger());
task_environment_.FastForwardBy(kFirstReportingWindow);
attribution_manager_->HandleSource(
SourceBuilder().SetSourceEventId(3).SetExpiry(kImpressionExpiry).Build());
attribution_manager_->HandleTrigger(DefaultTrigger());
task_environment_.FastForwardBy(kFirstReportingWindow);
// This one shouldn't be stored, as it will be retried.
attribution_manager_->HandleSource(
SourceBuilder().SetSourceEventId(4).SetExpiry(kImpressionExpiry).Build());
attribution_manager_->HandleTrigger(DefaultTrigger());
task_environment_.FastForwardBy(kFirstReportingWindow);
EXPECT_THAT(report_sender_->calls(), SizeIs(4));
report_sender_->RunCallbacksAndReset(
{SendResult::Status::kSent, SendResult::Status::kDropped,
SendResult::Status::kSent, SendResult::Status::kTransientFailure});
// kSent = 0.
histograms.ExpectBucketCount("Conversions.ReportSendOutcome", 0, 2);
// kFailed = 1.
histograms.ExpectBucketCount("Conversions.ReportSendOutcome", 1, 0);
// kDropped = 2.
histograms.ExpectBucketCount("Conversions.ReportSendOutcome", 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(CreateReportStatusIs(
AttributionTrigger::Result::kSuccess)))
.Times(3);
EXPECT_CALL(checkpoint, Call(1));
EXPECT_CALL(
observer,
OnTriggerHandled(AllOf(
DroppedReportIs(Optional(EventLevelDataIs(TriggerPriorityIs(1)))),
CreateReportStatusIs(
AttributionTrigger::Result::kSuccessDroppedLowerPriority))));
EXPECT_CALL(checkpoint, Call(2));
EXPECT_CALL(
observer,
OnTriggerHandled(AllOf(
DroppedReportIs(Optional(EventLevelDataIs(TriggerPriorityIs(-5)))),
CreateReportStatusIs(
AttributionTrigger::Result::kPriorityTooLow))));
EXPECT_CALL(checkpoint, Call(3));
EXPECT_CALL(
observer,
OnTriggerHandled(AllOf(
DroppedReportIs(Optional(EventLevelDataIs(TriggerPriorityIs(2)))),
CreateReportStatusIs(
AttributionTrigger::Result::kSuccessDroppedLowerPriority))));
EXPECT_CALL(
observer,
OnTriggerHandled(AllOf(
DroppedReportIs(Optional(EventLevelDataIs(TriggerPriorityIs(3)))),
CreateReportStatusIs(
AttributionTrigger::Result::kSuccessDroppedLowerPriority))));
}
attribution_manager_->HandleSource(
SourceBuilder().SetExpiry(kImpressionExpiry).Build());
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());
EXPECT_THAT(StoredReports(), SizeIs(i));
}
checkpoint.Call(1);
{
// This should replace the report with priority 1.
attribution_manager_->HandleTrigger(
TriggerBuilder().SetPriority(4).Build());
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());
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());
attribution_manager_->HandleTrigger(
TriggerBuilder().SetPriority(6).Build());
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());
attribution_manager_->HandleTrigger(DefaultTrigger());
base::RunLoop run_loop;
attribution_manager_->ClearData(
start, start + base::Minutes(1),
base::BindLambdaForTesting(
[match_url](const url::Origin& _) { return match_url; }),
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) {
attribution_manager_->HandleSource(
SourceBuilder().SetExpiry(kImpressionExpiry).Build());
attribution_manager_->HandleTrigger(DefaultTrigger());
std::vector<AttributionReport> reports = StoredReports();
EXPECT_THAT(reports, SizeIs(1));
EXPECT_THAT(report_sender_->calls(), IsEmpty());
attribution_manager_->SendReportsForWebUI(
{*(absl::get<AttributionReport::EventLevelData>(reports.front().data())
.id)},
base::DoNothing());
task_environment_.FastForwardBy(base::TimeDelta());
EXPECT_THAT(report_sender_->calls(), SizeIs(1));
}
TEST_F(AttributionManagerImplTest,
ConversionsSentFromUI_CallbackInvokedWhenAllDone) {
size_t callback_calls = 0;
attribution_manager_->HandleSource(
SourceBuilder().SetExpiry(kImpressionExpiry).Build());
attribution_manager_->HandleTrigger(DefaultTrigger());
attribution_manager_->HandleTrigger(DefaultTrigger());
std::vector<AttributionReport> reports = StoredReports();
EXPECT_THAT(reports, SizeIs(2));
EXPECT_THAT(report_sender_->calls(), IsEmpty());
attribution_manager_->SendReportsForWebUI(
{*(absl::get<AttributionReport::EventLevelData>(reports.front().data())
.id),
*(absl::get<AttributionReport::EventLevelData>(reports.back().data())
.id)},
base::BindLambdaForTesting([&]() { callback_calls++; }));
task_environment_.FastForwardBy(base::TimeDelta());
EXPECT_THAT(report_sender_->calls(), SizeIs(2));
EXPECT_EQ(callback_calls, 0u);
report_sender_->RunCallback(0, SendResult::Status::kSent);
task_environment_.FastForwardBy(base::TimeDelta());
EXPECT_EQ(callback_calls, 0u);
report_sender_->RunCallback(1, SendResult::Status::kTransientFailure);
task_environment_.FastForwardBy(base::TimeDelta());
EXPECT_EQ(callback_calls, 1u);
}
TEST_F(AttributionManagerImplTest, ExpiredReportsAtStartup_Delayed) {
attribution_manager_->HandleSource(
SourceBuilder().SetExpiry(kImpressionExpiry).Build());
attribution_manager_->HandleTrigger(DefaultTrigger());
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();
auto delay = storage_delegate_->GetOfflineReportDelayConfig();
EXPECT_THAT(StoredReports(),
ElementsAre(ReportTimeIs(AllOf(Ge(min_new_time + delay->min),
Le(min_new_time + delay->max)))));
EXPECT_THAT(report_sender_->calls(), IsEmpty());
}
TEST_F(AttributionManagerImplTest,
NonExpiredReportsQueuedAtStartup_NotDelayed) {
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());
attribution_manager_->HandleTrigger(DefaultTrigger());
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)));
EXPECT_THAT(report_sender_->calls(), IsEmpty());
}
TEST_F(AttributionManagerImplTest, SessionOnlyOrigins_DataDeletedAtShutdown) {
GURL session_only_origin("https://sessiononly.example");
auto impression =
SourceBuilder()
.SetImpressionOrigin(url::Origin::Create(session_only_origin))
.Build();
mock_storage_policy_->AddSessionOnly(session_only_origin);
attribution_manager_->HandleSource(impression);
attribution_manager_->HandleTrigger(DefaultTrigger());
EXPECT_THAT(StoredSources(), SizeIs(1));
EXPECT_THAT(StoredReports(), SizeIs(1));
ShutdownManager();
CreateManager();
EXPECT_THAT(StoredSources(), IsEmpty());
EXPECT_THAT(StoredReports(), IsEmpty());
}
TEST_F(AttributionManagerImplTest,
SessionOnlyOrigins_DeletedIfAnyOriginMatches) {
url::Origin session_only_origin =
url::Origin::Create(GURL("https://sessiononly.example"));
// Create impressions which each have the session only origin as one of
// impression/conversion/reporting origin.
auto impression1 =
SourceBuilder().SetImpressionOrigin(session_only_origin).Build();
auto impression2 =
SourceBuilder().SetReportingOrigin(session_only_origin).Build();
auto impression3 =
SourceBuilder().SetConversionOrigin(session_only_origin).Build();
// Create one impression which is not session only.
auto impression4 = SourceBuilder().Build();
mock_storage_policy_->AddSessionOnly(session_only_origin.GetURL());
attribution_manager_->HandleSource(impression1);
attribution_manager_->HandleSource(impression2);
attribution_manager_->HandleSource(impression3);
attribution_manager_->HandleSource(impression4);
EXPECT_THAT(StoredSources(), SizeIs(4));
ShutdownManager();
CreateManager();
// All session-only impressions should be deleted.
EXPECT_THAT(StoredSources(), SizeIs(1));
}
// 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());
EXPECT_THAT(StoredSources(), SizeIs(1));
attribution_manager_->HandleTrigger(TriggerBuilder().SetPriority(1).Build());
attribution_manager_->HandleTrigger(TriggerBuilder().SetPriority(1).Build());
attribution_manager_->HandleTrigger(TriggerBuilder().SetPriority(1).Build());
EXPECT_THAT(StoredReports(), SizeIs(3));
task_environment_.FastForwardBy(base::Days(7) - base::Minutes(30));
EXPECT_THAT(report_sender_->calls(), SizeIs(3));
report_sender_->RunCallbacksAndReset({SendResult::Status::kSent,
SendResult::Status::kSent,
SendResult::Status::kSent});
task_environment_.FastForwardBy(base::Minutes(5));
attribution_manager_->HandleTrigger(TriggerBuilder().SetPriority(2).Build());
task_environment_.FastForwardBy(base::Hours(1));
EXPECT_THAT(report_sender_->calls(), IsEmpty());
}
TEST_F(AttributionManagerImplTest, HandleTrigger_RecordsMetric) {
base::HistogramTester histograms;
attribution_manager_->HandleTrigger(DefaultTrigger());
EXPECT_THAT(StoredReports(), IsEmpty());
histograms.ExpectUniqueSample(
"Conversions.CreateReportStatus",
AttributionTrigger::Result::kNoMatchingImpressions, 1);
}
TEST_F(AttributionManagerImplTest, OnReportSent_NotifiesObservers) {
base::HistogramTester histograms;
attribution_manager_->HandleSource(SourceBuilder().Build());
attribution_manager_->HandleTrigger(DefaultTrigger());
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);
task_environment_.FastForwardBy(kFirstReportingWindow);
EXPECT_THAT(report_sender_->calls(), SizeIs(1));
report_sender_->RunCallbacksAndReset({SendResult::Status::kSent});
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);
Checkpoint checkpoint;
{
InSequence seq;
EXPECT_CALL(observer, OnSourcesChanged);
EXPECT_CALL(observer, OnReportsChanged).Times(0);
EXPECT_CALL(observer, OnSourceDeactivated).Times(0);
EXPECT_CALL(checkpoint, Call(1));
EXPECT_CALL(observer, OnSourcesChanged);
EXPECT_CALL(observer, OnReportsChanged);
EXPECT_CALL(observer, OnSourceDeactivated).Times(0);
EXPECT_CALL(checkpoint, Call(2));
EXPECT_CALL(observer, OnSourcesChanged);
EXPECT_CALL(observer, OnReportsChanged).Times(0);
EXPECT_CALL(observer,
OnSourceDeactivated(DeactivatedSource{
builder.BuildStored(),
DeactivatedSource::Reason::kReplacedByNewerSource}));
}
attribution_manager_->HandleSource(builder.Build());
EXPECT_THAT(StoredSources(), SizeIs(1));
checkpoint.Call(1);
attribution_manager_->HandleTrigger(DefaultTrigger());
EXPECT_THAT(StoredReports(), SizeIs(1));
checkpoint.Call(2);
attribution_manager_->HandleSource(
SourceBuilder().SetExpiry(kImpressionExpiry).SetSourceEventId(9).Build());
EXPECT_THAT(StoredSources(), SizeIs(1));
}
TEST_F(AttributionManagerImplTest, HandleTrigger_NotifiesObservers) {
MockAttributionObserver observer;
base::ScopedObservation<AttributionManager, AttributionObserver> observation(
&observer);
observation.Observe(attribution_manager_.get());
SourceBuilder builder;
builder.SetExpiry(kImpressionExpiry).SetSourceEventId(7);
Checkpoint checkpoint;
{
InSequence seq;
EXPECT_CALL(observer, OnSourcesChanged);
EXPECT_CALL(observer, OnReportsChanged).Times(0);
EXPECT_CALL(observer, OnSourceDeactivated).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);
}
EXPECT_CALL(observer, OnSourceDeactivated).Times(0);
EXPECT_CALL(checkpoint, Call(2));
EXPECT_CALL(observer, OnReportsChanged).Times(3);
EXPECT_CALL(checkpoint, Call(3));
EXPECT_CALL(observer, OnSourcesChanged);
EXPECT_CALL(observer, OnReportsChanged);
EXPECT_CALL(observer,
OnSourceDeactivated(DeactivatedSource{
builder.BuildStored(),
DeactivatedSource::Reason::kReachedAttributionLimit}));
}
attribution_manager_->HandleSource(builder.Build());
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(DefaultTrigger());
EXPECT_THAT(StoredReports(), SizeIs(i));
}
checkpoint.Call(2);
// Simulate the reports being sent and removed from storage.
task_environment_.FastForwardBy(kFirstReportingWindow);
EXPECT_THAT(report_sender_->calls(), SizeIs(3));
report_sender_->RunCallbacksAndReset({SendResult::Status::kSent,
SendResult::Status::kSent,
SendResult::Status::kSent});
EXPECT_THAT(StoredReports(), IsEmpty());
checkpoint.Call(3);
// The next report should cause the source to be deactivated; the report
// itself shouldn't be stored as we've already reached the maximum number of
// conversions per source.
attribution_manager_->HandleTrigger(DefaultTrigger());
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);
base::RunLoop run_loop;
attribution_manager_->ClearData(
base::Time::Min(), base::Time::Max(),
base::BindRepeating([](const url::Origin& _) { return false; }),
run_loop.QuitClosure());
run_loop.Run();
}
TEST_F(AttributionManagerImplTest, EmbedderDisallowsReporting_ReportNotSent) {
MockAttributionReportingContentBrowserClient browser_client;
EXPECT_CALL(
browser_client,
IsConversionMeasurementOperationAllowed(
_, ContentBrowserClient::ConversionMeasurementOperation::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;
attribution_manager_->HandleSource(
SourceBuilder().SetExpiry(kImpressionExpiry).Build());
attribution_manager_->HandleTrigger(DefaultTrigger());
EXPECT_THAT(StoredReports(), SizeIs(1));
MockAttributionObserver observer;
base::ScopedObservation<AttributionManager, AttributionObserver> observation(
&observer);
observation.Observe(attribution_manager_.get());
EXPECT_CALL(observer, OnReportSent(_, Field(&SendResult::status,
SendResult::Status::kDropped)));
task_environment_.FastForwardBy(kFirstReportingWindow);
EXPECT_THAT(StoredReports(), IsEmpty());
EXPECT_THAT(report_sender_->calls(), IsEmpty());
// kDropped = 2.
histograms.ExpectBucketCount("Conversions.ReportSendOutcome", 2, 1);
}
TEST_F(AttributionManagerImplTest, Offline_NoReportSent) {
attribution_manager_->HandleSource(
SourceBuilder().SetExpiry(kImpressionExpiry).Build());
attribution_manager_->HandleTrigger(DefaultTrigger());
EXPECT_THAT(StoredReports(), SizeIs(1));
SetConnectionTypeAndWaitForObserversToBeNotified(
network::mojom::ConnectionType::CONNECTION_NONE);
task_environment_.FastForwardBy(kFirstReportingWindow);
EXPECT_THAT(report_sender_->calls(), IsEmpty());
SetConnectionTypeAndWaitForObserversToBeNotified(
network::mojom::ConnectionType::CONNECTION_UNKNOWN);
EXPECT_THAT(report_sender_->calls(), SizeIs(1));
}
TEST_F(AttributionManagerImplTest,
OnlineConnectionTypeChanges_ReportTimesNotAdjusted) {
storage_delegate_->set_offline_report_delay_config(
AttributionStorageDelegate::OfflineReportDelayConfig{
.min = base::Minutes(1),
.max = base::Minutes(1),
});
attribution_manager_->HandleSource(
SourceBuilder().SetExpiry(kImpressionExpiry).Build());
attribution_manager_->HandleTrigger(DefaultTrigger());
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));
SetConnectionTypeAndWaitForObserversToBeNotified(
network::mojom::ConnectionType::CONNECTION_4G);
// Cause any scheduled tasks to run.
task_environment_.FastForwardBy(base::TimeDelta());
// This will fail with 0 calls if the report time was adjusted to +1 minute.
EXPECT_THAT(report_sender_->calls(), SizeIs(1));
}
TEST_F(AttributionManagerImplTest, TimeFromConversionToReportSendHistogram) {
base::HistogramTester histograms;
attribution_manager_->HandleSource(
SourceBuilder().SetExpiry(kImpressionExpiry).Build());
attribution_manager_->HandleTrigger(DefaultTrigger());
task_environment_.FastForwardBy(kFirstReportingWindow);
EXPECT_THAT(report_sender_->calls(), SizeIs(1));
histograms.ExpectUniqueSample("Conversions.TimeFromConversionToReportSend",
kFirstReportingWindow.InHours(), 1);
}
TEST_F(AttributionManagerImplTest, SendReport_RecordsExtraReportDelay2) {
base::HistogramTester histograms;
attribution_manager_->HandleSource(
SourceBuilder().SetExpiry(kImpressionExpiry).Build());
attribution_manager_->HandleTrigger(DefaultTrigger());
// 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));
EXPECT_THAT(report_sender_->calls(), IsEmpty());
SetConnectionTypeAndWaitForObserversToBeNotified(
network::mojom::ConnectionType::CONNECTION_UNKNOWN);
auto delay = storage_delegate_->GetOfflineReportDelayConfig();
task_environment_.FastForwardBy(delay->max);
EXPECT_THAT(report_sender_->calls(), SizeIs(1));
histograms.ExpectUniqueTimeSample("Conversions.ExtraReportDelay2",
base::Days(3) + delay->min, 1);
}
TEST_F(AttributionManagerImplTest, SendReportsFromWebUI_DoesNotRecordMetrics) {
base::HistogramTester histograms;
attribution_manager_->HandleSource(
SourceBuilder().SetExpiry(kImpressionExpiry).Build());
attribution_manager_->HandleTrigger(DefaultTrigger());
attribution_manager_->SendReportsForWebUI(
{AttributionReport::EventLevelData::Id(1)}, base::DoNothing());
task_environment_.FastForwardBy(base::TimeDelta());
EXPECT_THAT(report_sender_->calls(), SizeIs(1));
histograms.ExpectTotalCount("Conversions.ExtraReportDelay2", 0);
histograms.ExpectTotalCount("Conversions.TimeFromConversionToReportSend", 0);
}
// Regression test for https://crbug.com/1294519.
TEST_F(AttributionManagerImplTest, FakeReport_UpdatesSendReportTimer) {
storage_delegate_->set_randomized_response(
std::vector<AttributionStorageDelegate::FakeReport>{
{
.trigger_data = 0,
.report_time = base::Time::Now() + base::Days(1),
},
});
attribution_manager_->HandleSource(
SourceBuilder().SetExpiry(kImpressionExpiry).Build());
EXPECT_THAT(report_sender_->calls(), IsEmpty());
task_environment_.FastForwardBy(base::Days(1));
EXPECT_THAT(report_sender_->calls(), SizeIs(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 = url::Origin::Create(GURL("https://r1.test"));
const auto r2 = url::Origin::Create(GURL("https://r2.test"));
const AttributionManagerImpl::SourceOrTrigger kEvents[] = {
SourceBuilder()
.SetSourceEventId(1)
.SetDebugKey(11)
.SetReportingOrigin(r1)
.SetExpiry(kImpressionExpiry)
.Build(),
TriggerBuilder().SetTriggerData(2).SetReportingOrigin(r1).Build(),
TriggerBuilder()
.SetTriggerData(3)
.SetDebugKey(13)
.SetReportingOrigin(r2)
.Build(),
SourceBuilder()
.SetSourceEventId(4)
.SetDebugKey(14)
.SetReportingOrigin(r2)
.SetExpiry(kImpressionExpiry)
.Build(),
TriggerBuilder().SetTriggerData(5).SetReportingOrigin(r2).Build(),
};
for (const auto& event : kEvents) {
attribution_manager_->MaybeEnqueueEventForTesting(event);
}
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;
} kDebugKeyTestCases[] = {
{
"no debug key, no cookie",
absl::nullopt,
"https://r2.test",
absl::nullopt,
},
{
"has debug key, no cookie",
123,
"https://r2.test",
absl::nullopt,
},
{
"no debug key, has cookie",
absl::nullopt,
"https://r1.test",
absl::nullopt,
},
{
"has debug key, has cookie",
123,
"https://r1.test",
123,
},
};
} // namespace
TEST_F(AttributionManagerImplTest, HandleSource_DebugKey) {
cookie_checker_->AddOriginWithDebugCookieSet(
url::Origin::Create(GURL("https://r1.test")));
for (const auto& test_case : kDebugKeyTestCases) {
attribution_manager_->HandleSource(
SourceBuilder()
.SetReportingOrigin(
url::Origin::Create(GURL(test_case.reporting_origin)))
.SetDebugKey(test_case.input_debug_key)
.SetExpiry(kImpressionExpiry)
.Build());
EXPECT_THAT(StoredSources(),
ElementsAre(SourceDebugKeyIs(test_case.expected_debug_key)))
<< test_case.name;
attribution_manager_->ClearData(base::Time::Min(), base::Time::Max(),
base::NullCallback(), base::DoNothing());
}
}
TEST_F(AttributionManagerImplTest, HandleTrigger_DebugKey) {
cookie_checker_->AddOriginWithDebugCookieSet(
url::Origin::Create(GURL("https://r1.test")));
for (const auto& test_case : kDebugKeyTestCases) {
const auto reporting_origin =
url::Origin::Create(GURL(test_case.reporting_origin));
attribution_manager_->HandleSource(SourceBuilder()
.SetReportingOrigin(reporting_origin)
.SetExpiry(kImpressionExpiry)
.Build());
EXPECT_THAT(StoredSources(), SizeIs(1)) << test_case.name;
attribution_manager_->HandleTrigger(
TriggerBuilder()
.SetReportingOrigin(reporting_origin)
.SetDebugKey(test_case.input_debug_key)
.Build());
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(),
base::NullCallback(), base::DoNothing());
}
}
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, StorableSource::Result::kSuccess));
attribution_manager_->HandleSource(source);
EXPECT_THAT(StoredSources(), SizeIs(1));
}
TEST_F(AttributionManagerImplTest,
AggregateReportAssemblySucceeded_ReportSent) {
attribution_manager_->HandleSource(SourceBuilder().Build());
attribution_manager_->AddAggregatableAttributionForTesting(
AggregatableAttribution(
AttributionInfo(
SourceBuilder().SetSourceId(StoredSource::Id(1)).BuildStored(),
/*time=*/base::Time::Now(), /*debug_key=*/absl::nullopt),
/*report_time=*/base::Time::Now() + base::Hours(1),
/*contributions=*/
{AggregatableHistogramContribution(/*key=*/1, /*value=*/2)}));
// Make sure the report is not sent earlier than its report time.
task_environment_.FastForwardBy(base::Hours(1) - base::Microseconds(1));
EXPECT_THAT(aggregation_service_->calls(), IsEmpty());
task_environment_.FastForwardBy(base::Microseconds(1));
EXPECT_THAT(aggregation_service_->calls(), SizeIs(1));
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);
AggregatableReportSharedInfo shared_info(
base::Time::FromJavaTime(1234567890123),
/*privacy_budget_key=*/"example_pbk", DefaultExternalReportID(),
/*reporting_origin=*/
url::Origin::Create(GURL("https://example.reporting")),
AggregatableReportSharedInfo::DebugMode::kDisabled);
AggregatableReport report(std::move(payloads), shared_info.SerializeAsJson());
aggregation_service_->RunCallback(0, std::move(report),
AggregationService::AssemblyStatus::kOk);
EXPECT_THAT(report_sender_->calls(), SizeIs(1));
}
TEST_F(AttributionManagerImplTest,
AggregateReportAssemblyFailed_ReportNotSent) {
attribution_manager_->HandleSource(SourceBuilder().Build());
attribution_manager_->AddAggregatableAttributionForTesting(
AggregatableAttribution(
AttributionInfo(
SourceBuilder().SetSourceId(StoredSource::Id(1)).BuildStored(),
/*time=*/base::Time::Now(), /*debug_key=*/absl::nullopt),
/*report_time=*/base::Time::Now() + base::Hours(1),
/*contributions=*/
{AggregatableHistogramContribution(/*key=*/1, /*value=*/2)}));
// Make sure the report is not sent earlier than its report time.
task_environment_.FastForwardBy(base::Hours(1) - base::Microseconds(1));
EXPECT_THAT(aggregation_service_->calls(), IsEmpty());
task_environment_.FastForwardBy(base::Microseconds(1));
EXPECT_THAT(aggregation_service_->calls(), SizeIs(1));
aggregation_service_->RunCallback(
0, absl::nullopt, AggregationService::AssemblyStatus::kAssemblyFailed);
EXPECT_THAT(report_sender_->calls(), SizeIs(0));
}
TEST_F(AttributionManagerImplTest, AggregationServiceDisabled_ReportNotSent) {
ShutdownAggregationService();
attribution_manager_->HandleSource(SourceBuilder().Build());
attribution_manager_->AddAggregatableAttributionForTesting(
AggregatableAttribution(
AttributionInfo(
SourceBuilder().SetSourceId(StoredSource::Id(1)).BuildStored(),
/*time=*/base::Time::Now(), /*debug_key=*/absl::nullopt),
/*report_time=*/base::Time::Now() + base::Hours(1),
/*contributions=*/
{AggregatableHistogramContribution(/*key=*/1, /*value=*/2)}));
task_environment_.FastForwardBy(base::Hours(1));
EXPECT_THAT(report_sender_->calls(), IsEmpty());
}
TEST_F(AttributionManagerImplTest, EventAndAggregateReportsStored_BothSent) {
attribution_manager_->HandleSource(
SourceBuilder().SetExpiry(kImpressionExpiry).Build());
attribution_manager_->HandleTrigger(DefaultTrigger());
attribution_manager_->AddAggregatableAttributionForTesting(
AggregatableAttribution(
AttributionInfo(
SourceBuilder().SetSourceId(StoredSource::Id(1)).BuildStored(),
/*time=*/base::Time::Now(), /*debug_key=*/absl::nullopt),
/*report_time=*/base::Time::Now() + kFirstReportingWindow,
/*contributions=*/
{AggregatableHistogramContribution(/*key=*/1, /*value=*/2)}));
// Make sure the report is not sent earlier than its report time.
task_environment_.FastForwardBy(kFirstReportingWindow -
base::Microseconds(1));
EXPECT_THAT(aggregation_service_->calls(), IsEmpty());
task_environment_.FastForwardBy(base::Microseconds(1));
// Event-level report was sent.
EXPECT_THAT(report_sender_->calls(), SizeIs(1));
EXPECT_THAT(aggregation_service_->calls(), SizeIs(1));
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);
AggregatableReportSharedInfo shared_info(
base::Time::FromJavaTime(1234567890123),
/*privacy_budget_key=*/"example_pbk", DefaultExternalReportID(),
/*reporting_origin=*/
url::Origin::Create(GURL("https://example.reporting")),
AggregatableReportSharedInfo::DebugMode::kDisabled);
AggregatableReport report(std::move(payloads), shared_info.SerializeAsJson());
aggregation_service_->RunCallback(0, std::move(report),
AggregationService::AssemblyStatus::kOk);
// Aggregatable report was sent.
EXPECT_THAT(report_sender_->calls(), SizeIs(2));
}
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;
}
}
} // namespace content