blob: deb1c5457df501d4ecde479dd72ce8d93775694d [file] [log] [blame]
// Copyright 2024 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/loader/keep_alive_attribution_request_helper.h"
#include <memory>
#include <optional>
#include <utility>
#include <vector>
#include "base/check.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/strings/to_string.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "base/unguessable_token.h"
#include "components/attribution_reporting/attribution_src_request_status.h"
#include "components/attribution_reporting/constants.h"
#include "components/attribution_reporting/registration_eligibility.mojom-shared.h"
#include "components/attribution_reporting/suitable_origin.h"
#include "content/browser/attribution_reporting/attribution_background_registrations_id.h"
#include "content/browser/attribution_reporting/attribution_data_host_manager.h"
#include "content/browser/attribution_reporting/attribution_data_host_manager_impl.h"
#include "content/browser/attribution_reporting/attribution_os_level_manager.h"
#include "content/browser/attribution_reporting/attribution_suitable_context.h"
#include "content/browser/attribution_reporting/attribution_test_utils.h"
#include "content/browser/attribution_reporting/test/mock_attribution_manager.h"
#include "content/browser/storage_partition_impl.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/test/test_renderer_host.h"
#include "content/test/test_web_contents.h"
#include "net/http/http_response_headers.h"
#include "services/data_decoder/public/cpp/test_support/in_process_data_decoder.h"
#include "services/network/public/mojom/attribution.mojom-shared.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/tokens/tokens.h"
#include "url/gurl.h"
#include "url/origin.h"
namespace content {
class KeepAliveAttributionRequestHelperTestPeer {
public:
static BackgroundRegistrationsId GetHelperId(
KeepAliveAttributionRequestHelper& helper) {
return helper.id_;
}
};
namespace {
using ::attribution_reporting::AttributionSrcRequestStatus;
using ::attribution_reporting::SuitableOrigin;
using ::attribution_reporting::mojom::RegistrationEligibility;
using ::network::mojom::AttributionReportingEligibility;
using ::testing::_;
using ::testing::AllOf;
using ::testing::Property;
using ::testing::Return;
constexpr char kRegisterSourceJson[] =
R"json({"destination":"https://destination.example"})json";
constexpr char kRegisterTriggerJson[] = R"json({ })json";
using attribution_reporting::kAttributionReportingRegisterOsSourceHeader;
using attribution_reporting::kAttributionReportingRegisterOsTriggerHeader;
using attribution_reporting::kAttributionReportingRegisterSourceHeader;
using attribution_reporting::kAttributionReportingRegisterTriggerHeader;
class KeepAliveAttributionRequestHelperTest : public RenderViewHostTestHarness {
public:
KeepAliveAttributionRequestHelperTest()
: RenderViewHostTestHarness(
base::test::TaskEnvironment::MainThreadType::UI,
base::test::TaskEnvironment::TimeSource::MOCK_TIME),
scoped_api_state_setting_(
AttributionOsLevelManager::ScopedApiStateForTesting(
AttributionOsLevelManager::ApiState::kEnabled)) {
scoped_feature_list_.InitWithFeatures(
{blink::features::kKeepAliveInBrowserMigration,
blink::features::kAttributionReportingInBrowserMigration},
{});
}
void SetUp() override {
RenderViewHostTestHarness::SetUp();
auto mock_manager = std::make_unique<MockAttributionManager>();
auto data_host_manager =
std::make_unique<AttributionDataHostManagerImpl>(mock_manager.get());
mock_manager->SetDataHostManager(std::move(data_host_manager));
mock_attribution_manager_ = mock_manager.get();
OverrideAttributionManager(std::move(mock_manager));
test_web_contents()->GetPrimaryMainFrame()->InitializeRenderFrameIfNeeded();
}
void TearDown() override {
// Avoids dangling ref to `mock_attribution_manager_`.
mock_attribution_manager_ = nullptr;
OverrideAttributionManager(nullptr);
RenderViewHostTestHarness::TearDown();
}
protected:
TestWebContents* test_web_contents() {
return static_cast<TestWebContents*>(web_contents());
}
MockAttributionManager* mock_attribution_manager() {
return mock_attribution_manager_;
}
base::test::ScopedFeatureList& scoped_feature_list() {
return scoped_feature_list_;
}
std::unique_ptr<KeepAliveAttributionRequestHelper> CreateValidHelper(
const GURL& reporting_url,
AttributionReportingEligibility eligibility =
AttributionReportingEligibility::kEventSourceOrTrigger,
const std::optional<base::UnguessableToken>& attribution_src_token =
std::nullopt,
const GURL& context_url = GURL("https://secure_source.com")) {
test_web_contents()->NavigateAndCommit(context_url);
auto helper = KeepAliveAttributionRequestHelper::CreateIfNeeded(
eligibility, reporting_url, attribution_src_token,
"devtools-request-id",
*AttributionSuitableContext::Create(
test_web_contents()->GetPrimaryMainFrame()));
CHECK(helper);
return helper;
}
private:
void OverrideAttributionManager(std::unique_ptr<AttributionManager> manager) {
static_cast<StoragePartitionImpl*>(
browser_context()->GetDefaultStoragePartition())
->OverrideAttributionManagerForTesting(std::move(manager));
}
base::test::ScopedFeatureList scoped_feature_list_;
AttributionOsLevelManager::ScopedApiStateForTesting scoped_api_state_setting_;
raw_ptr<MockAttributionManager> mock_attribution_manager_;
data_decoder::test::InProcessDataDecoder in_process_data_decoder_;
};
TEST_F(KeepAliveAttributionRequestHelperTest, SingleResponse) {
const GURL source_url = GURL("https://secure_source.com");
const GURL reporting_url("https://report.test");
auto helper = CreateValidHelper(
reporting_url, AttributionReportingEligibility::kEventSourceOrTrigger,
/*attribution_src_token=*/std::nullopt, source_url);
EXPECT_CALL(
*mock_attribution_manager(),
HandleSource(
AllOf(ImpressionOriginIs(*SuitableOrigin::Create(source_url)),
ReportingOriginIs(*SuitableOrigin::Create(reporting_url))),
test_web_contents()->GetPrimaryMainFrame()->GetGlobalId()))
.Times(1);
auto headers = base::MakeRefCounted<net::HttpResponseHeaders>("");
headers->SetHeader(kAttributionReportingRegisterSourceHeader,
kRegisterSourceJson);
helper->OnReceiveResponse(headers.get());
// Wait for parsing to complete
task_environment()->FastForwardBy(base::TimeDelta());
}
TEST_F(KeepAliveAttributionRequestHelperTest, NavigationSource) {
const std::optional<base::UnguessableToken> attribution_src_token =
base::UnguessableToken(blink::AttributionSrcToken());
const GURL reporting_url("https://report.test");
auto helper = CreateValidHelper(
reporting_url, AttributionReportingEligibility::kNavigationSource,
attribution_src_token);
// HandleSource won't be called given that we haven't setup an actual
// navigation necessary for the registration to complete. The fact that it
// isn't called confirms that the token is being used.
EXPECT_CALL(*mock_attribution_manager(), HandleSource).Times(0);
auto headers = base::MakeRefCounted<net::HttpResponseHeaders>("");
headers->SetHeader(kAttributionReportingRegisterSourceHeader,
kRegisterSourceJson);
helper->OnReceiveResponse(headers.get());
task_environment()->FastForwardBy(base::TimeDelta());
}
TEST_F(KeepAliveAttributionRequestHelperTest, ExtraResponsesAreIgnored) {
const GURL reporting_url("https://report.test");
auto helper = CreateValidHelper(reporting_url);
EXPECT_CALL(*mock_attribution_manager(), HandleSource).Times(1);
auto headers_1 = base::MakeRefCounted<net::HttpResponseHeaders>("");
headers_1->SetHeader(kAttributionReportingRegisterSourceHeader,
kRegisterSourceJson);
helper->OnReceiveResponse(headers_1.get());
auto headers_2 = base::MakeRefCounted<net::HttpResponseHeaders>("");
headers_2->SetHeader(kAttributionReportingRegisterSourceHeader,
kRegisterSourceJson);
// This a valid response, however the helper processed a response, i.e.,
// OnReceiveResponse was previously called. As such, this response should be
// ignored.
helper->OnReceiveResponse(headers_2.get());
// Wait for parsing to complete
task_environment()->FastForwardBy(base::TimeDelta());
}
TEST_F(KeepAliveAttributionRequestHelperTest,
UnexpectedResponsesWillBeIgnored) {
const GURL reporting_url("https://report.test");
auto helper = CreateValidHelper(reporting_url);
// The second response should be ignored as a helper can only
// process a single response.
EXPECT_CALL(*mock_attribution_manager(), HandleSource).Times(1);
auto headers_1 = base::MakeRefCounted<net::HttpResponseHeaders>("");
headers_1->SetHeader(kAttributionReportingRegisterSourceHeader,
kRegisterSourceJson);
helper->OnReceiveResponse(headers_1.get());
auto headers_2 = base::MakeRefCounted<net::HttpResponseHeaders>("");
headers_2->SetHeader(kAttributionReportingRegisterSourceHeader,
kRegisterSourceJson);
helper->OnReceiveResponse(headers_2.get());
// Wait for parsing to complete
task_environment()->FastForwardBy(base::TimeDelta());
}
TEST_F(KeepAliveAttributionRequestHelperTest, NoAttributionHeader) {
const GURL reporting_url("https://report.test");
auto helper = CreateValidHelper(reporting_url);
EXPECT_CALL(*mock_attribution_manager(), HandleSource).Times(0);
auto headers = base::MakeRefCounted<net::HttpResponseHeaders>("");
headers->SetHeader("random-header", kRegisterSourceJson);
helper->OnReceiveResponse(headers.get());
// Wait for parsing even if none is expected to avoid false positive.
task_environment()->FastForwardBy(base::TimeDelta());
}
TEST_F(KeepAliveAttributionRequestHelperTest, Cleanup) {
const GURL reporting_url("https://report.test");
auto helper = CreateValidHelper(reporting_url);
AttributionDataHostManager* host =
mock_attribution_manager()->GetDataHostManager();
CHECK(host);
BackgroundRegistrationsId background_id =
KeepAliveAttributionRequestHelperTestPeer::GetHelperId(*helper);
const auto headers = base::MakeRefCounted<net::HttpResponseHeaders>("");
headers->SetHeader(kAttributionReportingRegisterSourceHeader,
kRegisterSourceJson);
const auto attempt_to_register_data = [&]() -> bool {
return host->NotifyBackgroundRegistrationData(background_id, &(*headers),
reporting_url);
};
// Call twice to show that we can call multiple times to register multiple
// headers.
ASSERT_TRUE(attempt_to_register_data());
ASSERT_TRUE(attempt_to_register_data());
// reset without having received a response.
helper.reset();
task_environment()->FastForwardBy(base::TimeDelta());
// The registration should have been completed upon the helper being reset.
// Once completed, it is not longer possible to register headers with the id.
EXPECT_FALSE(attempt_to_register_data());
}
TEST_F(KeepAliveAttributionRequestHelperTest, RedirectChain) {
// 0 won't have any header
const GURL reporting_url_0("https://report-0.test");
EXPECT_CALL(
*mock_attribution_manager(),
HandleSource(ReportingOriginIs(*SuitableOrigin::Create(reporting_url_0)),
_))
.Times(0);
// 1 will have an empty header
const GURL reporting_url_1("https://report-1.test");
EXPECT_CALL(
*mock_attribution_manager(),
HandleSource(ReportingOriginIs(*SuitableOrigin::Create(reporting_url_1)),
_))
.Times(0);
// 2 will register a source
const GURL reporting_url_2("https://report-2.test");
EXPECT_CALL(
*mock_attribution_manager(),
HandleSource(ReportingOriginIs(*SuitableOrigin::Create(reporting_url_2)),
_))
.Times(1);
// 3 will register a trigger
const GURL reporting_url_3("https://report-3.test");
EXPECT_CALL(*mock_attribution_manager(),
HandleTrigger(Property(&AttributionTrigger::reporting_origin,
*SuitableOrigin::Create(reporting_url_3)),
_))
.Times(1);
// 4 is not suitable, so it's response should be ignored.
const GURL reporting_url_4("http://report-4.test");
// 5 will register a source.
const GURL reporting_url_5("https://report-5.test");
EXPECT_CALL(
*mock_attribution_manager(),
HandleSource(ReportingOriginIs(*SuitableOrigin::Create(reporting_url_5)),
_))
.Times(1);
// 6 will register an OS source
const GURL reporting_url_6("https://report-6.test");
// 7 will register an OS trigger
const GURL reporting_url_7("https://report-7.test");
EXPECT_CALL(*mock_attribution_manager(), HandleOsRegistration).Times(2);
auto helper = CreateValidHelper(reporting_url_0);
helper->OnReceiveRedirect(/*headers=*/nullptr, reporting_url_1);
auto headers_1 = base::MakeRefCounted<net::HttpResponseHeaders>("");
helper->OnReceiveRedirect(headers_1.get(), reporting_url_2);
auto headers_2 = base::MakeRefCounted<net::HttpResponseHeaders>("");
headers_2->SetHeader(kAttributionReportingRegisterSourceHeader,
kRegisterSourceJson);
helper->OnReceiveRedirect(headers_2.get(), reporting_url_3);
auto headers_3 = base::MakeRefCounted<net::HttpResponseHeaders>("");
headers_3->SetHeader(kAttributionReportingRegisterTriggerHeader,
kRegisterTriggerJson);
helper->OnReceiveRedirect(headers_3.get(), reporting_url_4);
auto headers_4 = base::MakeRefCounted<net::HttpResponseHeaders>("");
headers_4->SetHeader(kAttributionReportingRegisterSourceHeader,
kRegisterSourceJson);
helper->OnReceiveRedirect(headers_4.get(), reporting_url_5);
auto headers_5 = base::MakeRefCounted<net::HttpResponseHeaders>("");
headers_5->SetHeader(kAttributionReportingRegisterSourceHeader,
kRegisterSourceJson);
helper->OnReceiveRedirect(headers_5.get(), reporting_url_6);
auto headers_6 = base::MakeRefCounted<net::HttpResponseHeaders>("");
headers_6->SetHeader(kAttributionReportingRegisterOsSourceHeader,
R"("https://r.test/x")");
helper->OnReceiveRedirect(headers_6.get(), reporting_url_7);
auto headers_7 = base::MakeRefCounted<net::HttpResponseHeaders>("");
headers_7->SetHeader(kAttributionReportingRegisterOsTriggerHeader,
R"("https://r.test/x")");
helper->OnReceiveResponse(headers_7.get());
// Wait for parsing to complete
task_environment()->FastForwardBy(base::TimeDelta());
}
TEST_F(KeepAliveAttributionRequestHelperTest, HelperNotNeeded) {
const GURL reporting_url("https://report.test");
{ // insecure context origin
const GURL source_url("http://insecure.test");
test_web_contents()->NavigateAndCommit(source_url);
auto context = AttributionSuitableContext::Create(
test_web_contents()->GetPrimaryMainFrame());
EXPECT_FALSE(context.has_value());
}
{ // ineligible request - kEmpty
const GURL source_url("https://secure.test");
test_web_contents()->NavigateAndCommit(source_url);
auto context = AttributionSuitableContext::Create(
test_web_contents()->GetPrimaryMainFrame());
ASSERT_TRUE(context.has_value());
auto helper = KeepAliveAttributionRequestHelper::CreateIfNeeded(
AttributionReportingEligibility::kEmpty, reporting_url,
/*attribution_src_token=*/std::nullopt, "devtools-request-id",
context.value());
EXPECT_EQ(helper, nullptr);
}
{ // kAttributionReportingInBrowserMigration disabled
scoped_feature_list().Reset();
scoped_feature_list().InitAndDisableFeature(
blink::features::kAttributionReportingInBrowserMigration);
const GURL source_url("https://secure.test");
test_web_contents()->NavigateAndCommit(source_url);
auto context = AttributionSuitableContext::Create(
test_web_contents()->GetPrimaryMainFrame());
ASSERT_TRUE(context.has_value());
auto helper = KeepAliveAttributionRequestHelper::CreateIfNeeded(
AttributionReportingEligibility::kEventSourceOrTrigger, reporting_url,
/*attribution_src_token=*/std::nullopt, "devtools-request-id",
context.value());
EXPECT_EQ(helper, nullptr);
}
}
TEST_F(KeepAliveAttributionRequestHelperTest, Eligibility) {
const struct {
AttributionReportingEligibility eligibility;
bool can_register_source;
bool can_register_trigger;
} kTestCases[] = {
{
.eligibility = AttributionReportingEligibility::kUnset,
.can_register_source = false,
.can_register_trigger = true,
},
{
.eligibility = AttributionReportingEligibility::kTrigger,
.can_register_source = false,
.can_register_trigger = true,
},
{
.eligibility = AttributionReportingEligibility::kEventSource,
.can_register_source = true,
.can_register_trigger = false,
},
{
.eligibility = AttributionReportingEligibility::kNavigationSource,
.can_register_source = true,
.can_register_trigger = false,
},
{
.eligibility = AttributionReportingEligibility::kEventSourceOrTrigger,
.can_register_source = true,
.can_register_trigger = true,
},
};
for (const auto& test_case : kTestCases) {
const GURL reporting_url_0("https://report-source." +
base::ToString(test_case.eligibility));
const GURL reporting_url_1("https://report-trigger." +
base::ToString(test_case.eligibility));
EXPECT_CALL(
*mock_attribution_manager(),
HandleSource(
ReportingOriginIs(*SuitableOrigin::Create(reporting_url_0)), _))
.Times(test_case.can_register_source);
EXPECT_CALL(
*mock_attribution_manager(),
HandleTrigger(Property(&AttributionTrigger::reporting_origin,
*SuitableOrigin::Create(reporting_url_1)),
_))
.Times(test_case.can_register_trigger);
auto helper = CreateValidHelper(reporting_url_0, test_case.eligibility);
auto headers_0 = base::MakeRefCounted<net::HttpResponseHeaders>("");
headers_0->SetHeader(kAttributionReportingRegisterSourceHeader,
kRegisterSourceJson);
helper->OnReceiveRedirect(headers_0.get(), reporting_url_1);
auto headers_1 = base::MakeRefCounted<net::HttpResponseHeaders>("");
headers_1->SetHeader(kAttributionReportingRegisterTriggerHeader,
kRegisterTriggerJson);
helper->OnReceiveResponse(headers_1.get());
}
// Wait for parsing to complete
task_environment()->FastForwardBy(base::TimeDelta());
}
TEST_F(KeepAliveAttributionRequestHelperTest, AttributionSrcRequestStatus) {
const struct {
const char* desc;
// Whether the request should be redirected.
bool redirect_request;
// Whether the request was redirected upon the first redirect.
bool second_redirect_request;
// Whether the request should succeed.
bool make_request_succeed;
std::vector<base::Bucket> expected;
} kTestCases[] = {
{
"succeeded",
/*redirect_request=*/false,
/*second_redirect_request=*/false,
/*make_request_succeed=*/true,
{base::Bucket(AttributionSrcRequestStatus::kRequested, 1),
base::Bucket(AttributionSrcRequestStatus::kReceived, 1)},
},
{
"failed",
/*redirect_request=*/false,
/*second_redirect_request=*/false,
/*make_request_succeed=*/false,
{base::Bucket(AttributionSrcRequestStatus::kRequested, 1),
base::Bucket(AttributionSrcRequestStatus::kFailed, 1)},
},
{
"redirected and succeeded",
/*redirect_request=*/true,
/*second_redirect_request=*/false,
/*make_request_succeed=*/true,
{base::Bucket(AttributionSrcRequestStatus::kRequested, 1),
base::Bucket(AttributionSrcRequestStatus::kRedirected, 1),
base::Bucket(AttributionSrcRequestStatus::kReceivedAfterRedirected,
1)},
},
{
"redirected and failed",
/*redirect_request=*/true,
/*second_redirect_request=*/false,
/*make_request_succeed=*/false,
{base::Bucket(AttributionSrcRequestStatus::kRequested, 1),
base::Bucket(AttributionSrcRequestStatus::kRedirected, 1),
base::Bucket(AttributionSrcRequestStatus::kFailedAfterRedirected,
1)},
},
{
"redirected twice and succeeded",
/*redirect_request=*/true,
/*second_redirect_request=*/true,
/*make_request_succeed=*/true,
{base::Bucket(AttributionSrcRequestStatus::kRequested, 1),
base::Bucket(AttributionSrcRequestStatus::kRedirected, 1),
base::Bucket(AttributionSrcRequestStatus::kReceivedAfterRedirected,
1)},
},
{
"redirected twice and failed",
/*redirect_request=*/true,
/*second_redirect_request=*/true,
/*make_request_succeed=*/false,
{base::Bucket(AttributionSrcRequestStatus::kRequested, 1),
base::Bucket(AttributionSrcRequestStatus::kRedirected, 1),
base::Bucket(AttributionSrcRequestStatus::kFailedAfterRedirected,
1)},
},
};
const GURL reporting_url("https://report.test");
const GURL redirect_url("https://report.test/redirect");
constexpr char kAttributionSrcNavigationRequestStatusMetric[] =
"Conversions.AttributionSrcRequestStatus.Navigation.Browser";
for (const bool is_navigation : {false, true}) {
SCOPED_TRACE(is_navigation);
for (const auto& test_case : kTestCases) {
SCOPED_TRACE(test_case.desc);
base::HistogramTester histograms;
auto helper = CreateValidHelper(
reporting_url,
is_navigation
? AttributionReportingEligibility::kNavigationSource
: AttributionReportingEligibility::kEventSourceOrTrigger);
if (test_case.redirect_request) {
auto headers = net::HttpResponseHeaders::TryToCreate("");
helper->OnReceiveRedirect(headers.get(), redirect_url);
if (test_case.second_redirect_request) {
auto second_headers = net::HttpResponseHeaders::TryToCreate("");
helper->OnReceiveRedirect(second_headers.get(), redirect_url);
}
}
if (test_case.make_request_succeed) {
auto headers = net::HttpResponseHeaders::TryToCreate("");
helper->OnReceiveResponse(headers.get());
} else {
helper->OnError();
}
if (is_navigation) {
EXPECT_THAT(histograms.GetAllSamples(
kAttributionSrcNavigationRequestStatusMetric),
base::BucketsAreArray(test_case.expected));
} else {
histograms.ExpectTotalCount(
kAttributionSrcNavigationRequestStatusMetric, 0);
}
}
}
}
TEST_F(KeepAliveAttributionRequestHelperTest, CreateIfNeeded_MetricRecorded) {
const struct {
const char* desc;
GURL context_url;
network::mojom::AttributionReportingEligibility eligibility;
std::optional<attribution_reporting::AttributionSrcRequestStatus> expected;
} kTestCases[] = {
{
"insecure-navigation",
GURL("http://insecure-source.com"),
network::mojom::AttributionReportingEligibility::kNavigationSource,
attribution_reporting::AttributionSrcRequestStatus::kDropped,
},
{
"secure-navigation",
GURL("https://secure-source.com"),
network::mojom::AttributionReportingEligibility::kNavigationSource,
attribution_reporting::AttributionSrcRequestStatus::kRequested,
},
{
"insecure-non-navigation",
GURL("http://insecure-source.com"),
network::mojom::AttributionReportingEligibility::
kEventSourceOrTrigger,
std::nullopt,
},
{
"secure-non-navigation",
GURL("https://secure-source.com"),
network::mojom::AttributionReportingEligibility::
kEventSourceOrTrigger,
std::nullopt,
},
};
constexpr char kAttributionSrcNavigationRequestStatusMetric[] =
"Conversions.AttributionSrcRequestStatus.Navigation.Browser";
const GURL reporting_url("https://report.test");
for (const auto& test_case : kTestCases) {
SCOPED_TRACE(test_case.desc);
test_web_contents()->NavigateAndCommit(test_case.context_url);
auto context = AttributionSuitableContext::Create(
test_web_contents()->GetPrimaryMainFrame());
base::HistogramTester histograms;
KeepAliveAttributionRequestHelper::CreateIfNeeded(
test_case.eligibility, reporting_url,
/*attribution_src_token=*/std::nullopt, "devtools-request-id", context);
if (test_case.expected.has_value()) {
histograms.ExpectUniqueSample(
kAttributionSrcNavigationRequestStatusMetric, *test_case.expected, 1);
} else {
histograms.ExpectTotalCount(kAttributionSrcNavigationRequestStatusMetric,
0);
}
}
}
} // namespace
} // namespace content