blob: 456502555a62b51c2ee1d240c4ec686c831d9822 [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_host.h"
#include <iterator>
#include <memory>
#include <optional>
#include <string>
#include <vector>
#include "base/memory/raw_ptr.h"
#include "base/metrics/metrics_hashes.h"
#include "base/run_loop.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "components/attribution_reporting/data_host.mojom.h"
#include "components/attribution_reporting/registration_eligibility.mojom.h"
#include "components/attribution_reporting/suitable_origin.h"
#include "components/metrics/dwa/dwa_recorder.h"
#include "content/browser/attribution_reporting/attribution_data_host_manager.h"
#include "content/browser/attribution_reporting/attribution_input_event.h"
#include "content/browser/attribution_reporting/attribution_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_data_host_manager.h"
#include "content/browser/attribution_reporting/test/mock_attribution_manager.h"
#include "content/browser/storage_partition_impl.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/global_routing_id.h"
#include "content/public/common/content_features.h"
#include "content/public/test/test_renderer_host.h"
#include "content/public/test/test_utils.h"
#include "content/test/navigation_simulator_impl.h"
#include "content/test/test_render_frame_host.h"
#include "content/test/test_web_contents.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "mojo/public/cpp/test_support/fake_message_dispatch_context.h"
#include "mojo/public/cpp/test_support/test_utils.h"
#include "services/network/public/cpp/permissions_policy/origin_with_possible_wildcards.h"
#include "services/network/public/cpp/permissions_policy/permissions_policy_declaration.h"
#include "services/network/public/mojom/permissions_policy/permissions_policy_feature.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 "third_party/blink/public/mojom/conversions/conversions.mojom.h"
#include "url/gurl.h"
#include "url/origin.h"
namespace content {
// Friended helper class to access private `receivers_` for test.
class AttributionHostTestPeer {
public:
static void SetCurrentTargetFrameForTesting(
AttributionHost* attribution_host,
RenderFrameHost* render_frame_host) {
attribution_host->receivers_.SetCurrentTargetFrameForTesting(
render_frame_host);
}
};
namespace {
using ::attribution_reporting::SuitableOrigin;
using ::testing::_;
using ::testing::AllOf;
using ::testing::Optional;
using ::testing::Pair;
using ::testing::Property;
using ::testing::Return;
using ::testing::UnorderedElementsAre;
using ::attribution_reporting::mojom::RegistrationEligibility;
const char kConversionUrl[] = "https://b.com";
constexpr bool kIsForBackgroundRequests = true;
const char kRedirectHeaderData[] =
"HTTP/1.1 301 Moved\0"
"Location: http://foopy/\0"
"\0";
class AttributionHostTest : public RenderViewHostTestHarness {
public:
AttributionHostTest() = default;
void SetUp() override {
feature_list_.InitAndEnableFeatureWithParameters(
blink::features::kFencedFrames, {{"implementation_type", "mparch"}});
RenderViewHostTestHarness::SetUp();
auto data_host_manager = std::make_unique<MockAttributionDataHostManager>();
mock_data_host_manager_ = data_host_manager.get();
auto mock_manager = std::make_unique<MockAttributionManager>();
mock_manager->SetDataHostManager(std::move(data_host_manager));
OverrideAttributionManager(std::move(mock_manager));
contents()->GetPrimaryMainFrame()->InitializeRenderFrameIfNeeded();
}
void TearDown() override {
// Avoids dangling ref to `mock_data_host_manager_`.
ClearAttributionManager();
RenderViewHostTestHarness::TearDown();
}
TestWebContents* contents() {
return static_cast<TestWebContents*>(web_contents());
}
blink::mojom::AttributionHost* attribution_host_mojom() {
return attribution_host();
}
AttributionHost* attribution_host() {
return AttributionHost::FromWebContents(web_contents());
}
void SetFencedFrameConfigPermissions(RenderFrameHost* fenced_frame) {
// Permissions in a fenced frame are tied to its urn:uuid-bound properties
// object. Because no navigation actually takes place in these tests, the
// code path that sets the permissions in the properties is never actually
// exercised. Instead, we need to manually inject the permissions into the
// properties object to ensure that the fenced frame loads with the needed
// permissions policy set.
FrameTreeNode* fenced_frame_node =
static_cast<RenderFrameHostImpl*>(fenced_frame)->frame_tree_node();
FencedFrameConfig new_config = FencedFrameConfig(GURL("about:blank"));
new_config.AddEffectiveEnabledPermissionForTesting(
network::mojom::PermissionsPolicyFeature::kAttributionReporting);
FencedFrameProperties new_props = FencedFrameProperties(new_config);
fenced_frame_node->set_fenced_frame_properties(new_props);
}
network::ParsedPermissionsPolicy RestrictivePermissionsPolicy(
const url::Origin& allowed_origin) {
return {network::ParsedPermissionsPolicyDeclaration(
network::mojom::PermissionsPolicyFeature::kAttributionReporting,
/*allowed_origins=*/
{*network::OriginWithPossibleWildcards::FromOrigin(allowed_origin)},
/*self_if_matches=*/std::nullopt,
/*matches_all_origins=*/false, /*matches_opaque_src=*/false)};
}
void ClearAttributionManager() {
mock_data_host_manager_ = nullptr;
OverrideAttributionManager(nullptr);
}
MockAttributionDataHostManager* mock_data_host_manager() {
return mock_data_host_manager_;
}
private:
void OverrideAttributionManager(std::unique_ptr<AttributionManager> manager) {
static_cast<StoragePartitionImpl*>(
browser_context()->GetDefaultStoragePartition())
->OverrideAttributionManagerForTesting(std::move(manager));
}
raw_ptr<MockAttributionDataHostManager> mock_data_host_manager_;
base::test::ScopedFeatureList feature_list_;
};
class ScopedAttributionHostTargetFrame {
public:
ScopedAttributionHostTargetFrame(AttributionHost* attribution_host,
RenderFrameHost* render_frame_host)
: attribution_host_(attribution_host) {
AttributionHostTestPeer::SetCurrentTargetFrameForTesting(attribution_host_,
render_frame_host);
}
~ScopedAttributionHostTargetFrame() {
AttributionHostTestPeer::SetCurrentTargetFrameForTesting(attribution_host_,
nullptr);
}
private:
const raw_ptr<AttributionHost> attribution_host_;
};
TEST_F(AttributionHostTest, NavigationWithNoImpression_Ignored) {
EXPECT_CALL(*mock_data_host_manager(), NotifyNavigationRegistrationStarted)
.Times(0);
contents()->NavigateAndCommit(GURL("https://secure_impression.com"));
NavigationSimulatorImpl::NavigateAndCommitFromDocument(GURL(kConversionUrl),
main_rfh());
}
TEST_F(AttributionHostTest, ValidAttributionSrc_ForwardedToManager) {
blink::Impression impression;
EXPECT_CALL(
*mock_data_host_manager(),
NotifyNavigationRegistrationStarted(
/*suitable_context=*/AllOf(
Property(&AttributionSuitableContext::context_origin,
*SuitableOrigin::Deserialize(
"https://secure_impression.com")),
Property(&AttributionSuitableContext::root_render_frame_id,
main_rfh()->GetGlobalId())),
impression.attribution_src_token,
/*navigation_id=*/_, /*devtools_request_id*/ _));
contents()->NavigateAndCommit(GURL("https://secure_impression.com"));
auto navigation = NavigationSimulatorImpl::CreateRendererInitiated(
GURL(kConversionUrl), main_rfh());
navigation->SetInitiatorFrame(main_rfh());
navigation->set_impression(std::move(impression));
navigation->Commit();
}
TEST_F(AttributionHostTest, ValidSourceRegistrations_ForwardedToManager) {
blink::Impression impression;
auto redirect_headers = base::MakeRefCounted<net::HttpResponseHeaders>(
std::string(kRedirectHeaderData, std::size(kRedirectHeaderData)));
auto headers = base::MakeRefCounted<net::HttpResponseHeaders>("");
const SuitableOrigin source_origin =
*SuitableOrigin::Deserialize("https://secure_impression.com");
const GURL b_url(kConversionUrl);
const SuitableOrigin b_origin = *SuitableOrigin::Create(b_url);
const GURL c_url("https://c.com");
const SuitableOrigin c_origin = *SuitableOrigin::Create(c_url);
const GURL d_url("https://d.com");
const SuitableOrigin d_origin = *SuitableOrigin::Create(d_url);
GlobalRenderFrameHostId frame_id = main_rfh()->GetGlobalId();
EXPECT_CALL(
*mock_data_host_manager(),
NotifyNavigationRegistrationStarted(
/*suitable_context=*/AllOf(
Property(&AttributionSuitableContext::context_origin,
source_origin),
Property(&AttributionSuitableContext::root_render_frame_id,
frame_id)),
impression.attribution_src_token,
/*navigation_id=*/_, /*devtools_request_id*/ _));
EXPECT_CALL(*mock_data_host_manager(),
NotifyNavigationRegistrationData(impression.attribution_src_token,
redirect_headers.get(),
/*reporting_url=*/b_url));
EXPECT_CALL(*mock_data_host_manager(),
NotifyNavigationRegistrationData(impression.attribution_src_token,
redirect_headers.get(),
/*reporting_url=*/c_url));
EXPECT_CALL(*mock_data_host_manager(),
NotifyNavigationRegistrationData(impression.attribution_src_token,
headers.get(),
/*reporting_url=*/d_url));
EXPECT_CALL(*mock_data_host_manager(),
NotifyNavigationRegistrationCompleted(
impression.attribution_src_token, _));
contents()->NavigateAndCommit(GURL("https://secure_impression.com"));
auto navigation =
NavigationSimulatorImpl::CreateRendererInitiated(b_url, main_rfh());
navigation->SetInitiatorFrame(main_rfh());
navigation->set_impression(std::move(impression));
navigation->SetRedirectHeaders(redirect_headers);
navigation->Redirect(c_url);
navigation->SetRedirectHeaders(redirect_headers);
navigation->Redirect(d_url);
navigation->SetResponseHeaders(headers);
navigation->Commit();
}
TEST_F(AttributionHostTest,
ValidAndInvalidSourceRegistrations_ForwardedToManager) {
blink::Impression impression;
auto redirect_headers = base::MakeRefCounted<net::HttpResponseHeaders>(
std::string(kRedirectHeaderData, std::size(kRedirectHeaderData)));
auto headers = base::MakeRefCounted<net::HttpResponseHeaders>("");
const SuitableOrigin source_origin =
*SuitableOrigin::Deserialize("https://secure_impression.com");
const GURL b_url(kConversionUrl);
const SuitableOrigin b_origin = *SuitableOrigin::Create(b_url);
const GURL c_url("https://c.com");
const SuitableOrigin c_origin = *SuitableOrigin::Create(c_url);
const GURL d_url("http://d.com");
GlobalRenderFrameHostId frame_id = main_rfh()->GetGlobalId();
EXPECT_CALL(
*mock_data_host_manager(),
NotifyNavigationRegistrationStarted(
/*suitable_context=*/AllOf(
Property(&AttributionSuitableContext::context_origin,
source_origin),
Property(&AttributionSuitableContext::root_render_frame_id,
frame_id)),
impression.attribution_src_token,
/*navigation_id=*/_, /*devtools_request_id*/ _));
EXPECT_CALL(*mock_data_host_manager(),
NotifyNavigationRegistrationData(impression.attribution_src_token,
redirect_headers.get(),
/*reporting_url=*/b_url));
EXPECT_CALL(*mock_data_host_manager(),
NotifyNavigationRegistrationData(impression.attribution_src_token,
redirect_headers.get(),
/*reporting_url=*/c_url));
// Expect no call for origin d as the reporting origin is not suitable.
// Expect that `NotifyNavigationRegistrationCompleted` gets called even if the
// last reporting_origin was not suitable.
EXPECT_CALL(*mock_data_host_manager(),
NotifyNavigationRegistrationCompleted(
impression.attribution_src_token, _));
contents()->NavigateAndCommit(GURL("https://secure_impression.com"));
auto navigation =
NavigationSimulatorImpl::CreateRendererInitiated(b_url, main_rfh());
navigation->SetInitiatorFrame(main_rfh());
navigation->set_impression(std::move(impression));
navigation->SetRedirectHeaders(redirect_headers);
navigation->Redirect(c_url);
navigation->SetRedirectHeaders(redirect_headers);
navigation->Redirect(d_url);
navigation->SetResponseHeaders(headers);
navigation->Commit();
}
TEST_F(AttributionHostTest, ImpressionInSubframe_Ignored) {
EXPECT_CALL(*mock_data_host_manager(), NotifyNavigationRegistrationStarted)
.Times(0);
contents()->NavigateAndCommit(GURL("https://secure_impression.com"));
// Create a subframe and use it as a target for the conversion registration
// mojo.
content::RenderFrameHostTester* rfh_tester =
content::RenderFrameHostTester::For(main_rfh());
content::RenderFrameHost* subframe = rfh_tester->AppendChild("subframe");
auto navigation = NavigationSimulatorImpl::CreateRendererInitiated(
GURL(kConversionUrl), subframe);
navigation->SetInitiatorFrame(main_rfh());
navigation->set_impression(blink::Impression());
navigation->Commit();
}
// Test that if we cannot access the initiator frame of the navigation, we
// ignore the associated impression but still notify when the navigation
// completes.
TEST_F(AttributionHostTest, ImpressionNavigationWithDeadInitiator_Ignored) {
EXPECT_CALL(*mock_data_host_manager(), NotifyNavigationRegistrationStarted)
.Times(0);
EXPECT_CALL(*mock_data_host_manager(), NotifyNavigationRegistrationData)
.Times(0);
EXPECT_CALL(*mock_data_host_manager(), NotifyNavigationRegistrationCompleted)
.Times(1);
base::HistogramTester histograms;
contents()->NavigateAndCommit(GURL("https://secure_impression.com"));
auto navigation = NavigationSimulatorImpl::CreateRendererInitiated(
GURL(kConversionUrl), main_rfh());
// This test explicitly requires no initiator frame being set.
navigation->SetInitiatorFrame(nullptr);
navigation->set_impression(blink::Impression());
navigation->Commit();
histograms.ExpectUniqueSample(
"Conversions.ImpressionNavigationHasDeadInitiator", true, 1);
}
TEST_F(AttributionHostTest,
AttributionSrcNavigationCommitsToErrorPage_Notified) {
blink::Impression impression;
EXPECT_CALL(
*mock_data_host_manager(),
NotifyNavigationRegistrationData(impression.attribution_src_token, _, _));
contents()->NavigateAndCommit(GURL("https://secure_impression.com"));
auto navigation = NavigationSimulatorImpl::CreateRendererInitiated(
GURL(kConversionUrl), main_rfh());
navigation->SetInitiatorFrame(main_rfh());
navigation->set_impression(std::move(impression));
navigation->Fail(net::ERR_FAILED);
navigation->CommitErrorPage();
}
TEST_F(AttributionHostTest, AttributionSrcNavigationAborts_Notified) {
blink::Impression impression;
EXPECT_CALL(
*mock_data_host_manager(),
NotifyNavigationRegistrationData(impression.attribution_src_token, _, _));
contents()->NavigateAndCommit(GURL("https://secure_impression.com"));
auto navigation = NavigationSimulatorImpl::CreateRendererInitiated(
GURL(kConversionUrl), main_rfh());
navigation->SetInitiatorFrame(main_rfh());
navigation->set_impression(std::move(impression));
navigation->AbortCommit();
}
TEST_F(AttributionHostTest,
CommittedOriginDiffersFromConversionDesintation_Notified) {
EXPECT_CALL(*mock_data_host_manager(), NotifyNavigationRegistrationData);
contents()->NavigateAndCommit(GURL("https://secure_impression.com"));
auto navigation = NavigationSimulatorImpl::CreateRendererInitiated(
GURL("https://different.com"), main_rfh());
navigation->SetInitiatorFrame(main_rfh());
navigation->set_impression(blink::Impression());
navigation->Commit();
}
namespace {
const char kLocalHost[] = "http://localhost";
struct OriginTrustworthyChecksTestCase {
const char* source_origin;
const char* destination_origin;
bool expected_valid;
};
const OriginTrustworthyChecksTestCase kOriginTrustworthyChecksTestCases[] = {
{.source_origin = kLocalHost,
.destination_origin = kLocalHost,
.expected_valid = true},
{.source_origin = "http://127.0.0.1",
.destination_origin = "http://127.0.0.1",
.expected_valid = true},
{.source_origin = kLocalHost,
.destination_origin = "http://insecure.com",
.expected_valid = true},
{.source_origin = "http://insecure.com",
.destination_origin = kLocalHost,
.expected_valid = false},
{.source_origin = "https://secure.com",
.destination_origin = "https://secure.com",
.expected_valid = true},
};
class AttributionHostOriginTrustworthyChecksTest
: public AttributionHostTest,
public ::testing::WithParamInterface<OriginTrustworthyChecksTestCase> {};
} // namespace
TEST_P(AttributionHostOriginTrustworthyChecksTest,
ImpressionNavigation_OriginTrustworthyChecksPerformed) {
const OriginTrustworthyChecksTestCase& test_case = GetParam();
EXPECT_CALL(*mock_data_host_manager(), NotifyNavigationRegistrationStarted)
.Times(test_case.expected_valid);
contents()->NavigateAndCommit(GURL(test_case.source_origin));
auto navigation = NavigationSimulatorImpl::CreateRendererInitiated(
GURL(test_case.destination_origin), main_rfh());
navigation->set_impression(blink::Impression());
navigation->SetInitiatorFrame(main_rfh());
navigation->Commit();
}
INSTANTIATE_TEST_SUITE_P(
,
AttributionHostOriginTrustworthyChecksTest,
::testing::ValuesIn(kOriginTrustworthyChecksTestCases));
TEST_F(AttributionHostTest, DataHost_RegisteredWithContext) {
EXPECT_CALL(
*mock_data_host_manager(),
RegisterDataHost(
_,
AllOf(Property(&AttributionSuitableContext::context_origin,
*SuitableOrigin::Deserialize("https://top.example")),
Property(&AttributionSuitableContext::root_render_frame_id,
main_rfh()->GetGlobalId())),
RegistrationEligibility::kSource, kIsForBackgroundRequests));
contents()->NavigateAndCommit(GURL("https://top.example"));
ScopedAttributionHostTargetFrame frame_scope(attribution_host(), main_rfh());
// Create a fake dispatch context to trigger a bad message in.
mojo::FakeMessageDispatchContext fake_dispatch_context;
mojo::test::BadMessageObserver bad_message_observer;
mojo::Remote<attribution_reporting::mojom::DataHost> data_host_remote;
attribution_host_mojom()->RegisterDataHost(
data_host_remote.BindNewPipeAndPassReceiver(),
RegistrationEligibility::kSource, kIsForBackgroundRequests,
/*reporting_origins=*/{});
// Run loop to allow the bad message code to run if a bad message was
// triggered.
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(bad_message_observer.got_bad_message());
}
// crbug.com/1378749.
TEST_F(AttributionHostTest, DISABLED_DataHostOnInsecurePage_BadMessage) {
contents()->NavigateAndCommit(GURL("http://top.example"));
ScopedAttributionHostTargetFrame frame_scope(attribution_host(), main_rfh());
// Create a fake dispatch context to trigger a bad message in.
mojo::FakeMessageDispatchContext fake_dispatch_context;
mojo::test::BadMessageObserver bad_message_observer;
mojo::Remote<attribution_reporting::mojom::DataHost> data_host_remote;
attribution_host_mojom()->RegisterDataHost(
data_host_remote.BindNewPipeAndPassReceiver(),
RegistrationEligibility::kSource, kIsForBackgroundRequests,
/*reporting_origins=*/{});
EXPECT_EQ(
"blink.mojom.AttributionHost can only be used with a secure top-level "
"frame.",
bad_message_observer.WaitForBadMessage());
}
// crbug.com/1378749.
TEST_F(AttributionHostTest,
DISABLED_NavigationDataHostOnInsecurePage_BadMessage) {
contents()->NavigateAndCommit(GURL("http://top.example"));
ScopedAttributionHostTargetFrame frame_scope(attribution_host(), main_rfh());
// Create a fake dispatch context to trigger a bad message in.
mojo::FakeMessageDispatchContext fake_dispatch_context;
mojo::test::BadMessageObserver bad_message_observer;
mojo::Remote<attribution_reporting::mojom::DataHost> data_host_remote;
attribution_host_mojom()->RegisterNavigationDataHost(
data_host_remote.BindNewPipeAndPassReceiver(),
blink::AttributionSrcToken());
EXPECT_EQ(
"blink.mojom.AttributionHost can only be used with a secure top-level "
"frame.",
bad_message_observer.WaitForBadMessage());
}
TEST_F(AttributionHostTest, DuplicateAttributionSrcToken_BadMessage) {
ON_CALL(*mock_data_host_manager(), RegisterNavigationDataHost)
.WillByDefault(Return(false));
contents()->NavigateAndCommit(GURL("https://top.example"));
ScopedAttributionHostTargetFrame frame_scope(attribution_host(), main_rfh());
// Create a fake dispatch context to trigger a bad message in.
mojo::FakeMessageDispatchContext fake_dispatch_context;
mojo::test::BadMessageObserver bad_message_observer;
mojo::Remote<attribution_reporting::mojom::DataHost> data_host_remote;
attribution_host_mojom()->RegisterNavigationDataHost(
data_host_remote.BindNewPipeAndPassReceiver(),
blink::AttributionSrcToken());
EXPECT_EQ(
"Renderer attempted to register a data host with a duplicate "
"AttributionSrcToken.",
bad_message_observer.WaitForBadMessage());
}
TEST_F(
AttributionHostTest,
NotifyNavigationWithBackgroundRegistrationsWillStart_DuplicateAttributionSrcToken_BadMessage) {
ON_CALL(*mock_data_host_manager(),
NotifyNavigationWithBackgroundRegistrationsWillStart)
.WillByDefault(Return(false));
contents()->NavigateAndCommit(GURL("https://top.example"));
ScopedAttributionHostTargetFrame frame_scope(attribution_host(), main_rfh());
// Create a fake dispatch context to trigger a bad message in.
mojo::FakeMessageDispatchContext fake_dispatch_context;
mojo::test::BadMessageObserver bad_message_observer;
mojo::Remote<attribution_reporting::mojom::DataHost> data_host_remote;
attribution_host_mojom()
->NotifyNavigationWithBackgroundRegistrationsWillStart(
blink::AttributionSrcToken(), /*expected_registrations=*/1);
EXPECT_EQ(
"Renderer attempted to notify of expected registrations with a duplicate "
"AttributionSrcToken or an invalid number of expected registrations.",
bad_message_observer.WaitForBadMessage());
}
TEST_F(
AttributionHostTest,
NotifyNavigationWithBackgroundRegistrationsWillStart_InsecureContext_Ignored) {
contents()->NavigateAndCommit(GURL("http://top.example"));
ScopedAttributionHostTargetFrame frame_scope(attribution_host(), main_rfh());
EXPECT_CALL(*mock_data_host_manager(),
NotifyNavigationWithBackgroundRegistrationsWillStart)
.Times(0);
mojo::Remote<attribution_reporting::mojom::DataHost> data_host_remote;
attribution_host_mojom()
->NotifyNavigationWithBackgroundRegistrationsWillStart(
blink::AttributionSrcToken(), /*expected_registrations=*/1);
}
TEST_F(AttributionHostTest, DataHostInSubframe_ContextIsOutermostFrame) {
EXPECT_CALL(
*mock_data_host_manager(),
RegisterDataHost(
_,
AllOf(Property(&AttributionSuitableContext::context_origin,
*SuitableOrigin::Deserialize("https://top.example")),
Property(&AttributionSuitableContext::root_render_frame_id,
main_rfh()->GetGlobalId())),
RegistrationEligibility::kSource, kIsForBackgroundRequests));
contents()->NavigateAndCommit(GURL("https://top.example"));
content::RenderFrameHostTester* rfh_tester =
content::RenderFrameHostTester::For(main_rfh());
content::RenderFrameHost* subframe = rfh_tester->AppendChild("subframe");
subframe = NavigationSimulatorImpl::NavigateAndCommitFromDocument(
GURL("https://subframe.example"), subframe);
ScopedAttributionHostTargetFrame frame_scope(attribution_host(), subframe);
// Create a fake dispatch context to trigger a bad message in.
mojo::FakeMessageDispatchContext fake_dispatch_context;
mojo::test::BadMessageObserver bad_message_observer;
mojo::Remote<attribution_reporting::mojom::DataHost> data_host_remote;
attribution_host_mojom()->RegisterDataHost(
data_host_remote.BindNewPipeAndPassReceiver(),
RegistrationEligibility::kSource, kIsForBackgroundRequests,
/*reporting_origins=*/{});
// Run loop to allow the bad message code to run if a bad message was
// triggered.
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(bad_message_observer.got_bad_message());
}
// crbug.com/1378749.
TEST_F(AttributionHostTest,
DISABLED_DataHostInSubframeOnInsecurePage_BadMessage) {
contents()->NavigateAndCommit(GURL("http://top.example"));
content::RenderFrameHostTester* rfh_tester =
content::RenderFrameHostTester::For(main_rfh());
content::RenderFrameHost* subframe = rfh_tester->AppendChild("subframe");
subframe = NavigationSimulatorImpl::NavigateAndCommitFromDocument(
GURL("https://subframe.example"), subframe);
ScopedAttributionHostTargetFrame frame_scope(attribution_host(), subframe);
// Create a fake dispatch context to trigger a bad message in.
mojo::FakeMessageDispatchContext fake_dispatch_context;
mojo::test::BadMessageObserver bad_message_observer;
mojo::Remote<attribution_reporting::mojom::DataHost> data_host_remote;
attribution_host_mojom()->RegisterDataHost(
data_host_remote.BindNewPipeAndPassReceiver(),
RegistrationEligibility::kSource, kIsForBackgroundRequests,
/*reporting_origins=*/{});
EXPECT_EQ(
"blink.mojom.AttributionHost can only be used with a secure top-level "
"frame.",
bad_message_observer.WaitForBadMessage());
}
TEST_F(AttributionHostTest, DataHost_RegisteredWithFencedFrame) {
EXPECT_CALL(
*mock_data_host_manager(),
RegisterDataHost(
_,
AllOf(Property(&AttributionSuitableContext::context_origin,
*SuitableOrigin::Deserialize("https://top.example")),
Property(&AttributionSuitableContext::root_render_frame_id,
main_rfh()->GetGlobalId())),
RegistrationEligibility::kSource, kIsForBackgroundRequests));
contents()->NavigateAndCommit(GURL("https://top.example"));
RenderFrameHost* fenced_frame =
RenderFrameHostTester::For(main_rfh())->AppendFencedFrame();
static_cast<RenderFrameHostImpl*>(fenced_frame)
->frame_tree_node()
->SetFencedFramePropertiesOpaqueAdsModeForTesting();
SetFencedFrameConfigPermissions(fenced_frame);
fenced_frame = NavigationSimulatorImpl::NavigateAndCommitFromDocument(
GURL("https://fencedframe.example"), fenced_frame);
ScopedAttributionHostTargetFrame frame_scope(attribution_host(),
fenced_frame);
// Create a fake dispatch context to trigger a bad message in.
mojo::FakeMessageDispatchContext fake_dispatch_context;
mojo::test::BadMessageObserver bad_message_observer;
mojo::Remote<attribution_reporting::mojom::DataHost> data_host_remote;
attribution_host_mojom()->RegisterDataHost(
data_host_remote.BindNewPipeAndPassReceiver(),
RegistrationEligibility::kSource, kIsForBackgroundRequests,
/*reporting_origins=*/{});
// Run loop to allow the bad message code to run if a bad message was
// triggered.
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(bad_message_observer.got_bad_message());
}
TEST_F(AttributionHostTest, ImpressionNavigation_FeaturePolicyChecked) {
blink::Impression impression;
static constexpr char kAllowedOriginUrl[] = "https://a.test";
const struct {
const char* url;
bool expected;
} kTestCases[] = {
{kAllowedOriginUrl, true},
{"https://b.test", false},
};
for (const auto& test_case : kTestCases) {
EXPECT_CALL(*mock_data_host_manager(), NotifyNavigationRegistrationStarted)
.Times(test_case.expected);
auto simulator1 = NavigationSimulatorImpl::CreateRendererInitiated(
GURL(test_case.url), main_rfh());
simulator1->SetPermissionsPolicyHeader(RestrictivePermissionsPolicy(
url::Origin::Create(GURL(kAllowedOriginUrl))));
simulator1->Commit();
auto simulator2 = NavigationSimulatorImpl::CreateRendererInitiated(
GURL(kConversionUrl), main_rfh());
simulator2->SetInitiatorFrame(main_rfh());
simulator2->set_impression(impression);
simulator2->Commit();
}
}
TEST_F(AttributionHostTest, RegisterDataHost_FeaturePolicyChecked) {
contents()->NavigateAndCommit(GURL("https://top.example"));
static constexpr char kAllowedOriginUrl[] = "https://a.test";
const struct {
const char* subframe_url;
bool expected;
} kTestCases[] = {
{kAllowedOriginUrl, true},
{"https://b.test", false},
};
for (const auto& test_case : kTestCases) {
EXPECT_CALL(*mock_data_host_manager(), RegisterDataHost)
.Times(test_case.expected);
content::RenderFrameHostTester* rfh_tester =
content::RenderFrameHostTester::For(main_rfh());
content::RenderFrameHost* subframe = rfh_tester->AppendChildWithPolicy(
"subframe", RestrictivePermissionsPolicy(
url::Origin::Create(GURL(kAllowedOriginUrl))));
subframe = NavigationSimulatorImpl::NavigateAndCommitFromDocument(
GURL(test_case.subframe_url), subframe);
ScopedAttributionHostTargetFrame frame_scope(attribution_host(), subframe);
mojo::Remote<attribution_reporting::mojom::DataHost> data_host_remote;
attribution_host_mojom()->RegisterDataHost(
data_host_remote.BindNewPipeAndPassReceiver(),
RegistrationEligibility::kSource, kIsForBackgroundRequests,
/*reporting_origins=*/{});
base::RunLoop().RunUntilIdle();
}
}
TEST_F(AttributionHostTest, RegisterNavigationDataHost_FeaturePolicyChecked) {
contents()->NavigateAndCommit(GURL("https://top.example"));
static constexpr char kAllowedOriginUrl[] = "https://a.test";
const struct {
const char* subframe_url;
bool expected;
} kTestCases[] = {
{kAllowedOriginUrl, true},
{"https://b.test", false},
};
for (const auto& test_case : kTestCases) {
if (test_case.expected) {
EXPECT_CALL(*mock_data_host_manager(), RegisterNavigationDataHost)
.WillOnce(Return(true));
} else {
EXPECT_CALL(*mock_data_host_manager(), RegisterNavigationDataHost)
.Times(0);
}
content::RenderFrameHostTester* rfh_tester =
content::RenderFrameHostTester::For(main_rfh());
content::RenderFrameHost* subframe = rfh_tester->AppendChildWithPolicy(
"subframe", RestrictivePermissionsPolicy(
url::Origin::Create(GURL(kAllowedOriginUrl))));
subframe = NavigationSimulatorImpl::NavigateAndCommitFromDocument(
GURL(test_case.subframe_url), subframe);
ScopedAttributionHostTargetFrame frame_scope(attribution_host(), subframe);
mojo::Remote<attribution_reporting::mojom::DataHost> data_host_remote;
attribution_host_mojom()->RegisterNavigationDataHost(
data_host_remote.BindNewPipeAndPassReceiver(),
blink::AttributionSrcToken());
base::RunLoop().RunUntilIdle();
}
}
TEST_F(
AttributionHostTest,
NotifyNavigationWithBackgroundRegistrationsWillStart_FeaturePolicyChecked) {
contents()->NavigateAndCommit(GURL("https://top.example"));
static constexpr char kAllowedOriginUrl[] = "https://a.test";
const struct {
const char* subframe_url;
bool expected;
} kTestCases[] = {
{kAllowedOriginUrl, true},
{"https://b.test", false},
};
for (const auto& test_case : kTestCases) {
if (test_case.expected) {
EXPECT_CALL(*mock_data_host_manager(),
NotifyNavigationWithBackgroundRegistrationsWillStart)
.WillOnce(Return(true));
} else {
EXPECT_CALL(*mock_data_host_manager(),
NotifyNavigationWithBackgroundRegistrationsWillStart)
.Times(0);
}
content::RenderFrameHostTester* rfh_tester =
content::RenderFrameHostTester::For(main_rfh());
content::RenderFrameHost* subframe = rfh_tester->AppendChildWithPolicy(
"subframe", RestrictivePermissionsPolicy(
url::Origin::Create(GURL(kAllowedOriginUrl))));
subframe = NavigationSimulatorImpl::NavigateAndCommitFromDocument(
GURL(test_case.subframe_url), subframe);
ScopedAttributionHostTargetFrame frame_scope(attribution_host(), subframe);
attribution_host_mojom()
->NotifyNavigationWithBackgroundRegistrationsWillStart(
blink::AttributionSrcToken(), /*expected_registrations=*/1);
base::RunLoop().RunUntilIdle();
}
}
TEST_F(AttributionHostTest, InsecureTaintTracking) {
blink::Impression impression;
auto redirect_headers = base::MakeRefCounted<net::HttpResponseHeaders>(
std::string(kRedirectHeaderData, std::size(kRedirectHeaderData)));
auto headers = base::MakeRefCounted<net::HttpResponseHeaders>("");
const SuitableOrigin source_origin =
*SuitableOrigin::Deserialize("https://secure_impression.com");
const GURL b_url(kConversionUrl);
const SuitableOrigin b_origin = *SuitableOrigin::Create(b_url);
const GURL insecure_url("http://insecure.com");
const GURL d_url("https://d.com");
const SuitableOrigin d_origin = *SuitableOrigin::Create(d_url);
EXPECT_CALL(
*mock_data_host_manager(),
NotifyNavigationRegistrationStarted(
AllOf(Property(&AttributionSuitableContext::context_origin,
source_origin),
Property(&AttributionSuitableContext::root_render_frame_id,
main_rfh()->GetGlobalId())),
impression.attribution_src_token,
/*navigation_id=*/_, /*devtools_request_id=*/_));
EXPECT_CALL(*mock_data_host_manager(),
NotifyNavigationRegistrationData(impression.attribution_src_token,
redirect_headers.get(),
/*reporting_url=*/b_url))
.WillOnce(Return(true));
EXPECT_CALL(*mock_data_host_manager(),
NotifyNavigationRegistrationData(impression.attribution_src_token,
headers.get(),
/*reporting_url=*/d_url))
.WillOnce(Return(true));
contents()->NavigateAndCommit(GURL("https://secure_impression.com"));
base::HistogramTester histograms;
auto navigation =
NavigationSimulatorImpl::CreateRendererInitiated(b_url, main_rfh());
navigation->SetInitiatorFrame(main_rfh());
navigation->set_impression(std::move(impression));
navigation->SetRedirectHeaders(redirect_headers);
navigation->Redirect(insecure_url);
navigation->SetRedirectHeaders(redirect_headers);
navigation->Redirect(d_url);
navigation->SetResponseHeaders(headers);
navigation->Commit();
histograms.ExpectUniqueSample("Conversions.IncrementalTaintingFailures", 1,
1);
}
TEST_F(AttributionHostTest, ClientBounce_RecordMetric) {
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndEnableFeature(metrics::dwa::kDwaFeature);
metrics::dwa::DwaRecorder::Get()->EnableRecording();
metrics::dwa::DwaRecorder::Get()->Purge();
ASSERT_THAT(metrics::dwa::DwaRecorder::Get()->GetEntriesForTesting(),
testing::IsEmpty());
contents()->NavigateAndCommit(GURL("https://top1.example"));
ScopedAttributionHostTargetFrame frame_scope(attribution_host(), main_rfh());
base::RunLoop run_loop_1;
EXPECT_CALL(*mock_data_host_manager(), RegisterDataHost)
.WillOnce([&run_loop_1]() { run_loop_1.Quit(); });
mojo::Remote<attribution_reporting::mojom::DataHost> data_host_remote;
attribution_host_mojom()->RegisterDataHost(
data_host_remote.BindNewPipeAndPassReceiver(),
RegistrationEligibility::kSource, kIsForBackgroundRequests,
// Counts 2 for https://a.r1.test, 1 for https://a.r2.test, and 1 for
// https://b.r1.test.
{url::Origin::Create(GURL("https://a.r1.test")),
url::Origin::Create(GURL("https://a.r2.test")),
url::Origin::Create(GURL("https://a.r1.test")),
url::Origin::Create(GURL("https://b.r1.test"))});
run_loop_1.Run();
base::RunLoop run_loop_2;
EXPECT_CALL(*mock_data_host_manager(), RegisterDataHost)
.WillOnce([&run_loop_2]() { run_loop_2.Quit(); });
data_host_remote.reset();
attribution_host_mojom()->RegisterDataHost(
data_host_remote.BindNewPipeAndPassReceiver(),
RegistrationEligibility::kSource, kIsForBackgroundRequests,
// Counts 1 for https://a.r1.test.
{url::Origin::Create(GURL("https://a.r1.test"))});
run_loop_2.Run();
auto navigation = NavigationSimulatorImpl::CreateRendererInitiated(
GURL("https://top2.example"), main_rfh());
navigation->SetInitiatorFrame(main_rfh());
navigation->SetHasUserGesture(false);
navigation->Commit();
static constexpr char kUserActivation1s[] = "UserActivation.1s";
static constexpr char kUserActivation5s[] = "UserActivation.5s";
static constexpr char kUserActivation10s[] = "UserActivation.10s";
static constexpr char kUserInteraction1s[] = "UserInteraction.1s";
static constexpr char kUserInteraction5s[] = "UserInteraction.5s";
static constexpr char kUserInteraction10s[] = "UserInteraction.10s";
ASSERT_THAT(metrics::dwa::DwaRecorder::Get()->GetEntriesForTesting().size(),
3);
EXPECT_THAT(metrics::dwa::DwaRecorder::Get()
->GetEntriesForTesting()
.at(0)
->event_hash,
base::HashMetricName("AttributionConversionsClientBounce"));
// DWA content sanitization extracts the eTLD+1 from this value, yielding
// "r1.test" for "a.r1.test".
EXPECT_THAT(metrics::dwa::DwaRecorder::Get()
->GetEntriesForTesting()
.at(0)
->content_hash,
base::HashMetricName("r1.test"));
EXPECT_THAT(
metrics::dwa::DwaRecorder::Get()->GetEntriesForTesting().at(0)->metrics,
UnorderedElementsAre(Pair(base::HashMetricName(kUserActivation1s), 3),
Pair(base::HashMetricName(kUserActivation5s), 3),
Pair(base::HashMetricName(kUserActivation10s), 3),
Pair(base::HashMetricName(kUserInteraction1s), 3),
Pair(base::HashMetricName(kUserInteraction5s), 3),
Pair(base::HashMetricName(kUserInteraction10s), 3)));
// Yielding "r2.test" for "a.r2.test".
EXPECT_THAT(metrics::dwa::DwaRecorder::Get()
->GetEntriesForTesting()
.at(1)
->content_hash,
base::HashMetricName("r2.test"));
EXPECT_THAT(
metrics::dwa::DwaRecorder::Get()->GetEntriesForTesting().at(1)->metrics,
UnorderedElementsAre(Pair(base::HashMetricName(kUserActivation1s), 1),
Pair(base::HashMetricName(kUserActivation5s), 1),
Pair(base::HashMetricName(kUserActivation10s), 1),
Pair(base::HashMetricName(kUserInteraction1s), 1),
Pair(base::HashMetricName(kUserInteraction5s), 1),
Pair(base::HashMetricName(kUserInteraction10s), 1)));
// Yielding "r1.test" for "b.r1.test".
EXPECT_THAT(metrics::dwa::DwaRecorder::Get()
->GetEntriesForTesting()
.at(2)
->content_hash,
base::HashMetricName("r1.test"));
EXPECT_THAT(
metrics::dwa::DwaRecorder::Get()->GetEntriesForTesting().at(2)->metrics,
UnorderedElementsAre(Pair(base::HashMetricName(kUserActivation1s), 1),
Pair(base::HashMetricName(kUserActivation5s), 1),
Pair(base::HashMetricName(kUserActivation10s), 1),
Pair(base::HashMetricName(kUserInteraction1s), 1),
Pair(base::HashMetricName(kUserInteraction5s), 1),
Pair(base::HashMetricName(kUserInteraction10s), 1)));
}
} // namespace
} // namespace content