blob: 9cb9afd0c81b662fab9541e8f718a857e3038597 [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_host.h"
#include <memory>
#include "base/test/metrics/histogram_tester.h"
#include "build/build_config.h"
#include "content/browser/attribution_reporting/attribution_manager.h"
#include "content/browser/attribution_reporting/attribution_test_utils.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/common/content_client.h"
#include "content/public/common/url_constants.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/test_support/fake_message_dispatch_context.h"
#include "mojo/public/cpp/test_support/test_utils.h"
#include "net/base/schemeful_site.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/mojom/conversions/conversions.mojom.h"
#include "url/gurl.h"
#include "url/origin.h"
#include "url/url_util.h"
namespace content {
class AttributionHostTestPeer {
public:
static std::unique_ptr<AttributionHost> CreateAttributionHost(
WebContents* web_contents,
std::unique_ptr<AttributionManager::Provider>
attribution_manager_provider) {
return base::WrapUnique(new AttributionHost(
web_contents, std::move(attribution_manager_provider)));
}
static void SetCurrentTargetFrameForTesting(
AttributionHost* conversion_host,
RenderFrameHost* render_frame_host) {
conversion_host->receivers_.SetCurrentTargetFrameForTesting(
render_frame_host);
}
};
namespace {
using ConversionMeasurementOperation =
::content::ContentBrowserClient::ConversionMeasurementOperation;
using testing::_;
using testing::AllOf;
using testing::InSequence;
using testing::IsNull;
using testing::Mock;
using testing::Pointee;
using testing::Property;
using testing::Return;
using Checkpoint = ::testing::MockFunction<void(int step)>;
const char kConversionUrl[] = "https://b.com";
const char kConversionUrlWithFragment[] = "https://b.com/#fragment";
const char kConversionUrlWithSubDomain[] = "https://sub.b.com";
blink::Impression CreateValidImpression() {
blink::Impression result;
result.conversion_destination = url::Origin::Create(GURL(kConversionUrl));
result.reporting_origin = url::Origin::Create(GURL("https://c.com"));
result.impression_data = 1UL;
result.priority = 10;
return result;
}
class AttributionHostTest : public RenderViewHostTestHarness {
public:
AttributionHostTest() = default;
void SetUp() override {
RenderViewHostTestHarness::SetUp();
conversion_host_ = AttributionHostTestPeer::CreateAttributionHost(
web_contents(), std::make_unique<TestManagerProvider>(&mock_manager_));
AttributionHost::SetReceiverImplForTesting(conversion_host_.get());
contents()->GetMainFrame()->InitializeRenderFrameIfNeeded();
}
void TearDown() override {
AttributionHost::SetReceiverImplForTesting(nullptr);
RenderViewHostTestHarness::TearDown();
}
TestWebContents* contents() {
return static_cast<TestWebContents*>(web_contents());
}
blink::mojom::ConversionHost* conversion_host_mojom() {
return conversion_host_.get();
}
AttributionHost* conversion_host() { return conversion_host_.get(); }
void SetCurrentTargetFrameForTesting(RenderFrameHost* render_frame_host) {
AttributionHostTestPeer::SetCurrentTargetFrameForTesting(
conversion_host_.get(), render_frame_host);
}
protected:
MockAttributionManager mock_manager_;
std::unique_ptr<AttributionHost> conversion_host_;
};
TEST_F(AttributionHostTest, ValidConversionInSubframe_NoBadMessage) {
EXPECT_CALL(mock_manager_,
HandleTrigger(Property(
&StorableTrigger::conversion_destination,
net::SchemefulSite(GURL("https://www.example.com")))));
contents()->NavigateAndCommit(GURL("https://www.example.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");
SetCurrentTargetFrameForTesting(subframe);
// Create a fake dispatch context to trigger a bad message in.
mojo::FakeMessageDispatchContext fake_dispatch_context;
mojo::test::BadMessageObserver bad_message_observer;
blink::mojom::ConversionPtr conversion = blink::mojom::Conversion::New();
conversion->reporting_origin =
url::Origin::Create(GURL("https://secure.com"));
conversion_host_mojom()->RegisterConversion(std::move(conversion));
// 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,
ConversionInSubframe_ConversionDestinationMatchesMainFrame) {
EXPECT_CALL(mock_manager_,
HandleTrigger(Property(
&StorableTrigger::conversion_destination,
net::SchemefulSite(GURL("https://www.example.com")))));
contents()->NavigateAndCommit(GURL("https://www.example.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");
subframe = NavigationSimulatorImpl::NavigateAndCommitFromDocument(
GURL("https://www.conversion.com"), subframe);
SetCurrentTargetFrameForTesting(subframe);
// Create a fake dispatch context to trigger a bad message in.
mojo::FakeMessageDispatchContext fake_dispatch_context;
mojo::test::BadMessageObserver bad_message_observer;
blink::mojom::ConversionPtr conversion = blink::mojom::Conversion::New();
conversion->reporting_origin =
url::Origin::Create(GURL("https://secure.com"));
conversion_host_mojom()->RegisterConversion(std::move(conversion));
// 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, ConversionInSubframeOnInsecurePage_BadMessage) {
EXPECT_CALL(mock_manager_, HandleTrigger).Times(0);
contents()->NavigateAndCommit(GURL("http://www.example.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");
subframe = NavigationSimulatorImpl::NavigateAndCommitFromDocument(
GURL("https://www.example.com"), subframe);
SetCurrentTargetFrameForTesting(subframe);
// Create a fake dispatch context to trigger a bad message in.
mojo::FakeMessageDispatchContext fake_dispatch_context;
mojo::test::BadMessageObserver bad_message_observer;
blink::mojom::ConversionPtr conversion = blink::mojom::Conversion::New();
conversion->reporting_origin =
url::Origin::Create(GURL("https://secure.com"));
conversion_host_mojom()->RegisterConversion(std::move(conversion));
EXPECT_EQ(
"blink.mojom.ConversionHost can only be used in secure contexts with a "
"secure conversion registration origin.",
bad_message_observer.WaitForBadMessage());
}
TEST_F(AttributionHostTest, ConversionInSubframe_ChecksCorrectOrigins) {
// Verifies that conversions from subframes use the correct origins when
// checking if the operation is allowed by the embedded.
MockAttributionReportingContentBrowserClient browser_client;
EXPECT_CALL(
browser_client,
IsConversionMeasurementOperationAllowed(
_, ConversionMeasurementOperation::kConversion, IsNull(),
Pointee(url::Origin::Create(GURL("https://www.example.com/"))),
Pointee(url::Origin::Create(GURL("https://report.example/")))))
.WillOnce(Return(false))
.WillOnce(Return(true));
ScopedContentBrowserClientSetting setting(&browser_client);
for (bool conversion_allowed : {false, true}) {
EXPECT_CALL(mock_manager_, HandleTrigger).Times(conversion_allowed);
contents()->NavigateAndCommit(GURL("https://www.example.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");
subframe = NavigationSimulatorImpl::NavigateAndCommitFromDocument(
GURL("https://www.another.com"), subframe);
SetCurrentTargetFrameForTesting(subframe);
blink::mojom::ConversionPtr conversion = blink::mojom::Conversion::New();
conversion->reporting_origin =
url::Origin::Create(GURL("https://report.example"));
conversion_host_mojom()->RegisterConversion(std::move(conversion));
Mock::VerifyAndClear(&mock_manager_);
}
}
TEST_F(AttributionHostTest, ConversionOnInsecurePage_BadMessage) {
EXPECT_CALL(mock_manager_, HandleTrigger).Times(0);
// Create a page with an insecure origin.
contents()->NavigateAndCommit(GURL("http://www.example.com"));
SetCurrentTargetFrameForTesting(main_rfh());
mojo::FakeMessageDispatchContext fake_dispatch_context;
mojo::test::BadMessageObserver bad_message_observer;
blink::mojom::ConversionPtr conversion = blink::mojom::Conversion::New();
conversion->reporting_origin =
url::Origin::Create(GURL("https://secure.com"));
// Message should be ignored because it was registered from an insecure page.
conversion_host_mojom()->RegisterConversion(std::move(conversion));
EXPECT_EQ(
"blink.mojom.ConversionHost can only be used in secure contexts with a "
"secure conversion registration origin.",
bad_message_observer.WaitForBadMessage());
}
TEST_F(AttributionHostTest, ConversionWithInsecureReportingOrigin_BadMessage) {
EXPECT_CALL(mock_manager_, HandleTrigger).Times(0);
contents()->NavigateAndCommit(GURL("https://www.example.com"));
SetCurrentTargetFrameForTesting(main_rfh());
mojo::FakeMessageDispatchContext fake_dispatch_context;
mojo::test::BadMessageObserver bad_message_observer;
blink::mojom::ConversionPtr conversion = blink::mojom::Conversion::New();
conversion->reporting_origin = url::Origin::Create(GURL("http://secure.com"));
// Message should be ignored because it was registered with an insecure
// redirect.
conversion_host_mojom()->RegisterConversion(std::move(conversion));
EXPECT_EQ(
"blink.mojom.ConversionHost can only be used in secure contexts with a "
"secure conversion registration origin.",
bad_message_observer.WaitForBadMessage());
}
TEST_F(AttributionHostTest, ValidConversion_NoBadMessage) {
EXPECT_CALL(mock_manager_, HandleTrigger);
// Create a page with a secure origin.
contents()->NavigateAndCommit(GURL("https://www.example.com"));
SetCurrentTargetFrameForTesting(main_rfh());
// Create a fake dispatch context to listen for bad messages.
mojo::FakeMessageDispatchContext fake_dispatch_context;
mojo::test::BadMessageObserver bad_message_observer;
blink::mojom::ConversionPtr conversion = blink::mojom::Conversion::New();
conversion->reporting_origin =
url::Origin::Create(GURL("https://secure.com"));
conversion_host_mojom()->RegisterConversion(std::move(conversion));
// 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, ValidConversionWithEmbedderDisable_NoConversion) {
EXPECT_CALL(mock_manager_, HandleTrigger).Times(0);
MockAttributionReportingContentBrowserClient browser_client;
EXPECT_CALL(
browser_client,
IsConversionMeasurementOperationAllowed(
_, ConversionMeasurementOperation::kConversion, IsNull(),
Pointee(url::Origin::Create(GURL("https://www.example.com/"))),
Pointee(url::Origin::Create(GURL("https://secure.com/")))))
.WillOnce(Return(false));
ScopedContentBrowserClientSetting setting(&browser_client);
// Create a page with a secure origin.
contents()->NavigateAndCommit(GURL("https://www.example.com"));
SetCurrentTargetFrameForTesting(main_rfh());
blink::mojom::ConversionPtr conversion = blink::mojom::Conversion::New();
conversion->reporting_origin =
url::Origin::Create(GURL("https://secure.com"));
conversion_host_mojom()->RegisterConversion(std::move(conversion));
}
TEST_F(AttributionHostTest, ValidImpressionWithEmbedderDisable_NoImpression) {
EXPECT_CALL(mock_manager_, HandleSource).Times(0);
MockAttributionReportingContentBrowserClient browser_client;
// This is called twice because the real AttributionHost is still active for
// the test.
EXPECT_CALL(
browser_client,
IsConversionMeasurementOperationAllowed(
_, ConversionMeasurementOperation::kImpression,
Pointee(url::Origin::Create(GURL("https://secure_impression.com/"))),
IsNull(), Pointee(url::Origin::Create(GURL("https://c.com/")))))
.Times(2)
.WillRepeatedly(Return(false));
ScopedContentBrowserClientSetting setting(&browser_client);
contents()->NavigateAndCommit(GURL("https://secure_impression.com"));
auto navigation = NavigationSimulatorImpl::CreateRendererInitiated(
GURL(kConversionUrl), main_rfh());
navigation->SetInitiatorFrame(main_rfh());
navigation->set_impression(CreateValidImpression());
navigation->Commit();
}
TEST_F(AttributionHostTest, Conversion_AssociatedWithConversionSite) {
// Verify that we use the domain of the page where the conversion occurred
// instead of the origin.
EXPECT_CALL(mock_manager_,
HandleTrigger(Property(
&StorableTrigger::conversion_destination,
net::SchemefulSite(GURL("https://conversion.com")))));
// Create a page with a secure origin.
contents()->NavigateAndCommit(GURL("https://sub.conversion.com"));
SetCurrentTargetFrameForTesting(main_rfh());
blink::mojom::ConversionPtr conversion = blink::mojom::Conversion::New();
conversion->reporting_origin =
url::Origin::Create(GURL("https://secure.com"));
conversion_host_mojom()->RegisterConversion(std::move(conversion));
}
TEST_F(AttributionHostTest, PerPageConversionMetrics) {
base::HistogramTester histograms;
contents()->NavigateAndCommit(GURL("https://www.example.com"));
// Initial document should not log metrics.
histograms.ExpectTotalCount("Conversions.RegisteredConversionsPerPage", 0);
histograms.ExpectTotalCount(
"Conversions.UniqueReportingOriginsPerPage.Conversions", 0);
SetCurrentTargetFrameForTesting(main_rfh());
blink::mojom::ConversionPtr conversion = blink::mojom::Conversion::New();
conversion->reporting_origin =
url::Origin::Create(GURL("https://secure.com"));
for (size_t i = 0u; i < 8u; i++) {
EXPECT_CALL(mock_manager_, HandleTrigger);
conversion_host_mojom()->RegisterConversion(conversion->Clone());
Mock::VerifyAndClear(&mock_manager_);
}
EXPECT_CALL(mock_manager_, HandleTrigger);
conversion->reporting_origin =
url::Origin::Create(GURL("https://anothersecure.com"));
conversion_host_mojom()->RegisterConversion(conversion->Clone());
Mock::VerifyAndClear(&mock_manager_);
// Same document navs should not reset the counter.
contents()->NavigateAndCommit(GURL("https://www.example.com#hash"));
histograms.ExpectTotalCount("Conversions.RegisteredConversionsPerPage", 0);
histograms.ExpectTotalCount(
"Conversions.UniqueReportingOriginsPerPage.Conversions", 0);
// Re-navigating should reset the counter.
contents()->NavigateAndCommit(GURL("https://www.example-next.com"));
// TODO(johnidel): This test creates a second conversion host which gets
// injected with a TestManager. However, the AttributionHost owned by the
// WebContents is still active for this test, and will record a zero sample in
// this histogram. Consider modifying this test suite so that we do not have
// metrics being recorded in multiple places.
histograms.ExpectBucketCount("Conversions.RegisteredConversionsPerPage", 9,
1);
histograms.ExpectBucketCount("Conversions.RegisteredConversionsPerPage", 1,
0);
histograms.ExpectBucketCount(
"Conversions.UniqueReportingOriginsPerPage.Conversions", 2, 1);
}
TEST_F(AttributionHostTest, NoManager_NoPerPageConversionMetrics) {
// Replace the AttributionHost on the WebContents with one that is backed by a
// null AttributionManager.
conversion_host_ = AttributionHostTestPeer::CreateAttributionHost(
web_contents(), std::make_unique<TestManagerProvider>(nullptr));
AttributionHost::SetReceiverImplForTesting(conversion_host_.get());
contents()->NavigateAndCommit(GURL("https://www.example.com"));
base::HistogramTester histograms;
SetCurrentTargetFrameForTesting(main_rfh());
blink::mojom::ConversionPtr conversion = blink::mojom::Conversion::New();
conversion->reporting_origin =
url::Origin::Create(GURL("https://secure.com"));
conversion_host_mojom()->RegisterConversion(std::move(conversion));
// Navigate again to trigger histogram code.
contents()->NavigateAndCommit(GURL("https://www.example-next.com"));
histograms.ExpectBucketCount("Conversions.RegisteredConversionsPerPage", 1,
0);
histograms.ExpectTotalCount(
"Conversions.UniqueReportingOriginsPerPage.Conversions", 0);
}
TEST_F(AttributionHostTest, PerPageImpressionMetrics) {
base::HistogramTester histograms;
contents()->NavigateAndCommit(GURL("https://www.example.com"));
// Initial document should not log metrics.
histograms.ExpectTotalCount(
"Conversions.UniqueReportingOriginsPerPage.Impressions", 0);
SetCurrentTargetFrameForTesting(main_rfh());
blink::Impression impression = CreateValidImpression();
for (size_t i = 0u; i < 8u; i++) {
EXPECT_CALL(mock_manager_, HandleSource);
conversion_host_mojom()->RegisterImpression(impression);
// Run loop to allow the bad message code to run if a bad message was
// triggered.
base::RunLoop().RunUntilIdle();
Mock::VerifyAndClear(&mock_manager_);
}
EXPECT_CALL(mock_manager_, HandleSource);
impression.reporting_origin =
url::Origin::Create(GURL("https://anothersecure.com"));
conversion_host_mojom()->RegisterImpression(impression);
// Run loop to allow the bad message code to run if a bad message was
// triggered.
base::RunLoop().RunUntilIdle();
Mock::VerifyAndClear(&mock_manager_);
// Same document navs should not reset the counter.
contents()->NavigateAndCommit(GURL("https://www.example.com#hash"));
histograms.ExpectTotalCount(
"Conversions.UniqueReportingOriginsPerPage.Impressions", 0);
// Re-navigating should reset the counter.
contents()->NavigateAndCommit(GURL("https://www.example-next.com"));
histograms.ExpectBucketCount(
"Conversions.UniqueReportingOriginsPerPage.Impressions", 2, 1);
}
TEST_F(AttributionHostTest, NoManager_NoPerPageImpressionMetrics) {
// Replace the AttributionHost on the WebContents with one that is backed by a
// null AttributionManager.
conversion_host_ = AttributionHostTestPeer::CreateAttributionHost(
web_contents(), std::make_unique<TestManagerProvider>(nullptr));
AttributionHost::SetReceiverImplForTesting(conversion_host_.get());
contents()->NavigateAndCommit(GURL("https://www.example.com"));
base::HistogramTester histograms;
SetCurrentTargetFrameForTesting(main_rfh());
blink::Impression impression = CreateValidImpression();
conversion_host_mojom()->RegisterImpression(std::move(impression));
// Navigate again to trigger histogram code.
contents()->NavigateAndCommit(GURL("https://www.example-next.com"));
histograms.ExpectTotalCount(
"Conversions.UniqueReportingOriginsPerPage.Impressions", 0);
}
TEST_F(AttributionHostTest, NavigationWithImpression_PerPageImpressionMetrics) {
base::HistogramTester histograms;
contents()->NavigateAndCommit(GURL("https://www.example.com"));
// Initial document should not log metrics.
histograms.ExpectTotalCount(
"Conversions.UniqueReportingOriginsPerPage.Impressions", 0);
blink::Impression impression = CreateValidImpression();
for (size_t i = 0u; i < 2u; i++) {
auto navigation = NavigationSimulatorImpl::CreateRendererInitiated(
GURL(kConversionUrl), main_rfh());
navigation->SetInitiatorFrame(main_rfh());
navigation->set_impression(impression);
navigation->Commit();
}
// Navigate again to trigger histogram code.
contents()->NavigateAndCommit(GURL("https://www.example-next.com"));
histograms.ExpectBucketCount(
"Conversions.UniqueReportingOriginsPerPage.Impressions", 1, 2);
}
TEST_F(AttributionHostTest, NavigationWithNoImpression_Ignored) {
EXPECT_CALL(mock_manager_, HandleSource).Times(0);
contents()->NavigateAndCommit(GURL("https://secure_impression.com"));
NavigationSimulatorImpl::NavigateAndCommitFromDocument(GURL(kConversionUrl),
main_rfh());
}
TEST_F(AttributionHostTest, ValidImpression_ForwardedToManager) {
EXPECT_CALL(mock_manager_, HandleSource);
contents()->NavigateAndCommit(GURL("https://secure_impression.com"));
auto navigation = NavigationSimulatorImpl::CreateRendererInitiated(
GURL(kConversionUrl), main_rfh());
navigation->SetInitiatorFrame(main_rfh());
navigation->set_impression(CreateValidImpression());
navigation->Commit();
}
TEST_F(AttributionHostTest, ImpressionWithNoManagerAvilable_NoCrash) {
// Replace the AttributionHost on the WebContents with one that is backed by a
// null AttributionManager.
conversion_host_ = AttributionHostTestPeer::CreateAttributionHost(
web_contents(), std::make_unique<TestManagerProvider>(nullptr));
AttributionHost::SetReceiverImplForTesting(conversion_host_.get());
auto navigation = NavigationSimulatorImpl::CreateRendererInitiated(
GURL(kConversionUrl), main_rfh());
navigation->SetInitiatorFrame(main_rfh());
navigation->set_impression(CreateValidImpression());
navigation->Commit();
}
TEST_F(AttributionHostTest, ImpressionInSubframe_Ignored) {
EXPECT_CALL(mock_manager_, HandleSource).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(CreateValidImpression());
navigation->Commit();
}
// Test that if we cannot access the initiator frame of the navigation, we
// ignore the associated impression.
TEST_F(AttributionHostTest, ImpressionNavigationWithDeadInitiator_Ignored) {
EXPECT_CALL(mock_manager_, HandleSource).Times(0);
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(CreateValidImpression());
navigation->Commit();
histograms.ExpectUniqueSample(
"Conversions.ImpressionNavigationHasDeadInitiator", true, 2);
}
TEST_F(AttributionHostTest, ImpressionNavigationCommitsToErrorPage_Ignored) {
EXPECT_CALL(mock_manager_, HandleSource).Times(0);
contents()->NavigateAndCommit(GURL("https://secure_impression.com"));
auto navigation = NavigationSimulatorImpl::CreateRendererInitiated(
GURL(kConversionUrl), main_rfh());
navigation->SetInitiatorFrame(main_rfh());
navigation->set_impression(CreateValidImpression());
navigation->Fail(net::ERR_FAILED);
navigation->CommitErrorPage();
}
TEST_F(AttributionHostTest, ImpressionNavigationAborts_Ignored) {
EXPECT_CALL(mock_manager_, HandleSource).Times(0);
contents()->NavigateAndCommit(GURL("https://secure_impression.com"));
auto navigation = NavigationSimulatorImpl::CreateRendererInitiated(
GURL(kConversionUrl), main_rfh());
navigation->SetInitiatorFrame(main_rfh());
navigation->set_impression(CreateValidImpression());
navigation->AbortCommit();
}
TEST_F(AttributionHostTest,
CommittedOriginDiffersFromConversionDesintation_Ignored) {
EXPECT_CALL(mock_manager_, HandleSource).Times(0);
contents()->NavigateAndCommit(GURL("https://secure_impression.com"));
auto navigation = NavigationSimulatorImpl::CreateRendererInitiated(
GURL("https://different.com"), main_rfh());
navigation->SetInitiatorFrame(main_rfh());
navigation->set_impression(CreateValidImpression());
navigation->Commit();
}
TEST_F(AttributionHostTest,
ImpressionNavigation_OriginTrustworthyChecksPerformed) {
const char kLocalHost[] = "http://localhost";
struct {
std::string impression_origin;
std::string conversion_origin;
std::string reporting_origin;
bool impression_expected;
} kTestCases[] = {
{.impression_origin = kLocalHost,
.conversion_origin = kLocalHost,
.reporting_origin = kLocalHost,
.impression_expected = true},
{.impression_origin = "http://127.0.0.1",
.conversion_origin = "http://127.0.0.1",
.reporting_origin = "http://127.0.0.1",
.impression_expected = true},
{.impression_origin = kLocalHost,
.conversion_origin = kLocalHost,
.reporting_origin = "http://insecure.com",
.impression_expected = false},
{.impression_origin = kLocalHost,
.conversion_origin = "http://insecure.com",
.reporting_origin = kLocalHost,
.impression_expected = false},
{.impression_origin = "http://insecure.com",
.conversion_origin = kLocalHost,
.reporting_origin = kLocalHost,
.impression_expected = false},
{.impression_origin = "https://secure.com",
.conversion_origin = "https://secure.com",
.reporting_origin = "https://secure.com",
.impression_expected = true},
};
for (const auto& test_case : kTestCases) {
EXPECT_CALL(mock_manager_, HandleSource)
.Times(test_case.impression_expected);
contents()->NavigateAndCommit(GURL(test_case.impression_origin));
auto navigation = NavigationSimulatorImpl::CreateRendererInitiated(
GURL(test_case.conversion_origin), main_rfh());
blink::Impression impression;
impression.conversion_destination =
url::Origin::Create(GURL(test_case.conversion_origin));
impression.reporting_origin =
url::Origin::Create(GURL(test_case.reporting_origin));
navigation->set_impression(impression);
navigation->SetInitiatorFrame(main_rfh());
navigation->Commit();
Mock::VerifyAndClear(&mock_manager_);
}
}
TEST_F(AttributionHostTest,
ImpressionInSubframe_ImpressionOriginMatchesTopPageOrigin) {
EXPECT_CALL(mock_manager_,
HandleSource(Property(
&StorableSource::impression_origin,
url::Origin::Create(GURL("https://www.example.com")))));
contents()->NavigateAndCommit(GURL("https://www.example.com"));
// Create a subframe and use it as a target for the impression registration
// mojo.
content::RenderFrameHostTester* rfh_tester =
content::RenderFrameHostTester::For(main_rfh());
content::RenderFrameHost* subframe = rfh_tester->AppendChild("subframe");
subframe = NavigationSimulatorImpl::NavigateAndCommitFromDocument(
GURL("https://www.impression.com"), subframe);
SetCurrentTargetFrameForTesting(subframe);
// Create a fake dispatch context to trigger a bad message in.
mojo::FakeMessageDispatchContext fake_dispatch_context;
mojo::test::BadMessageObserver bad_message_observer;
const blink::Impression impression = CreateValidImpression();
conversion_host_mojom()->RegisterImpression(impression);
// 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, ValidImpression_NoBadMessage) {
EXPECT_CALL(mock_manager_,
HandleSource(AllOf(Property(&StorableSource::source_type,
StorableSource::SourceType::kEvent),
Property(&StorableSource::priority, 10))));
// Create a page with a secure origin.
contents()->NavigateAndCommit(GURL("https://www.example.com"));
SetCurrentTargetFrameForTesting(main_rfh());
// Create a fake dispatch context to listen for bad messages.
mojo::FakeMessageDispatchContext fake_dispatch_context;
mojo::test::BadMessageObserver bad_message_observer;
const blink::Impression impression = CreateValidImpression();
conversion_host_mojom()->RegisterImpression(impression);
// 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, RegisterImpression_RecordsAllowedMetric) {
// Create a page with a secure origin.
contents()->NavigateAndCommit(GURL("https://www.example.com"));
SetCurrentTargetFrameForTesting(main_rfh());
MockAttributionReportingContentBrowserClient browser_client;
EXPECT_CALL(browser_client,
IsConversionMeasurementOperationAllowed(
_, ConversionMeasurementOperation::kImpression, Pointee(_),
IsNull(), Pointee(_)))
.WillOnce(Return(true))
.WillOnce(Return(false));
ScopedContentBrowserClientSetting setting(&browser_client);
const struct {
bool want_allowed;
} kTestCases[] = {
{true},
{false},
};
for (const auto& test_case : kTestCases) {
base::HistogramTester histograms;
conversion_host_mojom()->RegisterImpression(CreateValidImpression());
histograms.ExpectUniqueSample("Conversions.RegisterImpressionAllowed",
test_case.want_allowed, 1);
}
}
TEST_F(AttributionHostTest, RegisterConversion_RecordsAllowedMetric) {
// Create a page with a secure origin.
contents()->NavigateAndCommit(GURL("https://www.example.com"));
SetCurrentTargetFrameForTesting(main_rfh());
MockAttributionReportingContentBrowserClient browser_client;
EXPECT_CALL(browser_client,
IsConversionMeasurementOperationAllowed(
_, ConversionMeasurementOperation::kConversion, IsNull(),
Pointee(_), Pointee(_)))
.WillOnce(Return(true))
.WillOnce(Return(false));
ScopedContentBrowserClientSetting setting(&browser_client);
const struct {
bool want_allowed;
} kTestCases[] = {
{true},
{false},
};
for (const auto& test_case : kTestCases) {
base::HistogramTester histograms;
blink::mojom::ConversionPtr conversion = blink::mojom::Conversion::New();
conversion->reporting_origin =
url::Origin::Create(GURL("https://secure.com"));
conversion_host_mojom()->RegisterConversion(std::move(conversion));
histograms.ExpectUniqueSample("Conversions.RegisterConversionAllowed",
test_case.want_allowed, 1);
}
}
// In pre-loaded CCT navigations, the attribution can arrive after the
// navigation begins but before it's committed. Currently only used on Android
// but should work cross-platform.
TEST_F(AttributionHostTest, AndroidConversion_DuringNavigation) {
Checkpoint checkpoint;
{
InSequence seq;
EXPECT_CALL(mock_manager_, HandleSource).Times(0);
EXPECT_CALL(checkpoint, Call(1));
EXPECT_CALL(mock_manager_, HandleSource).Times(0);
EXPECT_CALL(checkpoint, Call(2));
EXPECT_CALL(mock_manager_, HandleSource);
}
std::string origin(
#if defined(OS_ANDROID)
"android-app:com.any.app");
url::ScopedSchemeRegistryForTests scoped_registry;
url::AddStandardScheme(kAndroidAppScheme, url::SCHEME_WITH_HOST);
#else
"https://secure.com");
#endif
auto navigation = NavigationSimulatorImpl::CreateBrowserInitiated(
GURL(kConversionUrl), contents());
navigation->Start();
checkpoint.Call(1);
conversion_host()->ReportAttributionForCurrentNavigation(
url::Origin::Create(GURL(origin)), CreateValidImpression());
checkpoint.Call(2);
navigation->Commit();
}
// In pre-loaded CCT navigations, the attribution can arrive after the
// navigation completes. Currently only used on Android but should work
// cross-platform.
TEST_F(AttributionHostTest, AndroidConversion_AfterNavigation) {
Checkpoint checkpoint;
{
InSequence seq;
EXPECT_CALL(mock_manager_, HandleSource).Times(0);
EXPECT_CALL(checkpoint, Call(1));
EXPECT_CALL(mock_manager_, HandleSource);
EXPECT_CALL(checkpoint, Call(2));
EXPECT_CALL(mock_manager_, HandleSource).Times(0);
}
std::string origin(
#if defined(OS_ANDROID)
"android-app:com.any.app");
url::ScopedSchemeRegistryForTests scoped_registry;
url::AddStandardScheme(kAndroidAppScheme, url::SCHEME_WITH_HOST);
#else
"https://secure.com");
#endif
auto navigation = NavigationSimulatorImpl::CreateBrowserInitiated(
GURL(kConversionUrl), contents());
navigation->Commit();
checkpoint.Call(1);
conversion_host()->ReportAttributionForCurrentNavigation(
url::Origin::Create(GURL(origin)), CreateValidImpression());
checkpoint.Call(2);
// Make sure we don't allow repeated attributions for the same navigation.
conversion_host()->ReportAttributionForCurrentNavigation(
url::Origin::Create(GURL(origin)), CreateValidImpression());
}
TEST_F(AttributionHostTest, AndroidConversion_AfterNavigation_SubDomain) {
EXPECT_CALL(mock_manager_, HandleSource);
std::string origin(
#if defined(OS_ANDROID)
"android-app:com.any.app");
url::ScopedSchemeRegistryForTests scoped_registry;
url::AddStandardScheme(kAndroidAppScheme, url::SCHEME_WITH_HOST);
#else
"https://secure.com");
#endif
auto navigation = NavigationSimulatorImpl::CreateBrowserInitiated(
GURL(kConversionUrlWithSubDomain), contents());
navigation->Commit();
conversion_host()->ReportAttributionForCurrentNavigation(
url::Origin::Create(GURL(origin)), CreateValidImpression());
}
// In pre-loaded CCT navigations, the attribution can arrive after the
// navigation completes, but the destination must match the attribution.
TEST_F(AttributionHostTest,
AndroidConversion_AfterNavigation_WrongDestination) {
EXPECT_CALL(mock_manager_, HandleSource).Times(0);
std::string origin(
#if defined(OS_ANDROID)
"android-app:com.any.app");
url::ScopedSchemeRegistryForTests scoped_registry;
url::AddStandardScheme(kAndroidAppScheme, url::SCHEME_WITH_HOST);
#else
"https://secure.com");
#endif
auto bad_navigation = NavigationSimulatorImpl::CreateBrowserInitiated(
GURL("https://other.com"), contents());
bad_navigation->Commit();
conversion_host()->ReportAttributionForCurrentNavigation(
url::Origin::Create(GURL(origin)), CreateValidImpression());
// Navigating to the correct URL after navigation to the wrong one still
// shouldn't allow the attribution.
auto good_navigation = NavigationSimulatorImpl::CreateBrowserInitiated(
GURL(kConversionUrl), contents());
good_navigation->Commit();
}
// Ensure we don't re-use pending Impressions after an aborted commit. Currently
// only used on Android but should work cross-platform.
TEST_F(AttributionHostTest, AndroidConversion_NavigationAborted) {
EXPECT_CALL(mock_manager_, HandleSource).Times(0);
std::string origin(
#if defined(OS_ANDROID)
"android-app:com.any.app");
url::ScopedSchemeRegistryForTests scoped_registry;
url::AddStandardScheme(kAndroidAppScheme, url::SCHEME_WITH_HOST);
#else
"https://secure.com");
#endif
auto navigation_abort = NavigationSimulatorImpl::CreateBrowserInitiated(
GURL(kConversionUrl), contents());
navigation_abort->Start();
conversion_host()->ReportAttributionForCurrentNavigation(
url::Origin::Create(GURL(origin)), CreateValidImpression());
navigation_abort->AbortCommit();
auto navigation_commit = NavigationSimulatorImpl::CreateBrowserInitiated(
GURL(kConversionUrl), contents());
navigation_commit->Commit();
}
// Ensure we don't re-use pending Impressions after an Error page commit.
// Currently only used on Android but should work cross-platform.
TEST_F(AttributionHostTest, AndroidConversion_NavigationError) {
EXPECT_CALL(mock_manager_, HandleSource).Times(0);
std::string origin(
#if defined(OS_ANDROID)
"android-app:com.any.app");
url::ScopedSchemeRegistryForTests scoped_registry;
url::AddStandardScheme(kAndroidAppScheme, url::SCHEME_WITH_HOST);
#else
"https://secure.com");
#endif
auto navigation_error = NavigationSimulatorImpl::CreateBrowserInitiated(
GURL(kConversionUrl), contents());
navigation_error->Start();
conversion_host()->ReportAttributionForCurrentNavigation(
url::Origin::Create(GURL(origin)), CreateValidImpression());
navigation_error->Fail(net::ERR_UNEXPECTED);
navigation_error->CommitErrorPage();
auto navigation_commit = NavigationSimulatorImpl::CreateBrowserInitiated(
GURL(kConversionUrl), contents());
navigation_commit->Commit();
}
// We don't allow attributions before a navigation begins. Currently only used
// on Android but should work cross-platform.
TEST_F(AttributionHostTest, AndroidConversion_BeforeNavigation) {
EXPECT_CALL(mock_manager_, HandleSource).Times(0);
std::string origin(
#if defined(OS_ANDROID)
"android-app:com.any.app");
url::ScopedSchemeRegistryForTests scoped_registry;
url::AddStandardScheme(kAndroidAppScheme, url::SCHEME_WITH_HOST);
#else
"https://secure.com");
#endif
auto navigation = NavigationSimulatorImpl::CreateBrowserInitiated(
GURL(kConversionUrl), contents());
conversion_host()->ReportAttributionForCurrentNavigation(
url::Origin::Create(GURL(origin)), CreateValidImpression());
navigation->Commit();
}
// We ignore same-document navigations.
TEST_F(AttributionHostTest, AndroidConversion_SameDocument) {
EXPECT_CALL(mock_manager_, HandleSource);
std::string origin(
#if defined(OS_ANDROID)
"android-app:com.any.app");
url::ScopedSchemeRegistryForTests scoped_registry;
url::AddStandardScheme(kAndroidAppScheme, url::SCHEME_WITH_HOST);
#else
"https://secure.com");
#endif
auto navigation = NavigationSimulatorImpl::CreateRendererInitiated(
GURL(kConversionUrl), main_rfh());
navigation->Commit();
auto navigation2 = NavigationSimulatorImpl::CreateRendererInitiated(
GURL(kConversionUrlWithFragment), main_rfh());
navigation2->CommitSameDocument();
conversion_host()->ReportAttributionForCurrentNavigation(
url::Origin::Create(GURL(origin)), CreateValidImpression());
}
#if defined(OS_ANDROID)
TEST_F(AttributionHostTest, AndroidConversion) {
EXPECT_CALL(mock_manager_, HandleSource);
url::ScopedSchemeRegistryForTests scoped_registry;
url::AddStandardScheme(kAndroidAppScheme, url::SCHEME_WITH_HOST);
auto navigation = NavigationSimulatorImpl::CreateBrowserInitiated(
GURL(kConversionUrl), contents());
navigation->set_initiator_origin(
url::Origin::Create(GURL("android-app:com.any.app")));
navigation->set_impression(CreateValidImpression());
navigation->Commit();
}
TEST_F(AttributionHostTest, AndroidConversion_BadScheme) {
EXPECT_CALL(mock_manager_, HandleSource).Times(0);
auto navigation = NavigationSimulatorImpl::CreateBrowserInitiated(
GURL(kConversionUrl), contents());
navigation->set_initiator_origin(
url::Origin::Create(GURL("https://com.any.app")));
navigation->set_impression(CreateValidImpression());
navigation->Commit();
}
#endif
} // namespace
} // namespace content