blob: 69ccf2ccebedcdf1d12c9ef55fd54ca7ab83bcc1 [file] [log] [blame]
// Copyright 2022 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/renderer_host/pending_beacon_host.h"
#include <vector>
#include "base/files/file_path.h"
#include "base/memory/scoped_refptr.h"
#include "base/strings/stringprintf.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "content/browser/renderer_host/pending_beacon_service.h"
#include "content/public/browser/permission_result.h"
#include "content/public/test/mock_permission_manager.h"
#include "content/public/test/test_browser_context.h"
#include "content/public/test/test_renderer_host.h"
#include "mojo/public/cpp/system/functions.h"
#include "net/http/http_request_headers.h"
#include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h"
#include "services/network/public/mojom/fetch_api.mojom.h"
#include "services/network/test/test_url_loader_factory.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/permissions/permission_utils.h"
#include "third_party/blink/public/mojom/frame/pending_beacon.mojom-shared.h"
#include "third_party/blink/public/mojom/frame/pending_beacon.mojom.h"
#include "url/origin.h"
namespace content {
struct MockClientBeacon {
MockClientBeacon(const MockClientBeacon&) = delete;
MockClientBeacon& operator=(const MockClientBeacon&) = delete;
MockClientBeacon() = default;
mojo::Remote<blink::mojom::PendingBeacon> remote;
};
class PendingBeaconHostTestBase
: public RenderViewHostTestHarness,
public ::testing::WithParamInterface<std::string> {
public:
PendingBeaconHostTestBase(const PendingBeaconHostTestBase&) = delete;
PendingBeaconHostTestBase& operator=(const PendingBeaconHostTestBase&) =
delete;
PendingBeaconHostTestBase() = default;
protected:
// Creates a new instance of PendingBeaconHost, which uses a new instance of
// TestURLLoaderFactory stored at `test_url_loader_factory_`.
// The network requests made by the returned PendingBeaconHost will go through
// `test_url_loader_factory_` which is useful for examining requests.
PendingBeaconHost* CreateHost() {
SetPermissionStatus(blink::PermissionType::BACKGROUND_SYNC,
blink::mojom::PermissionStatus::GRANTED);
test_url_loader_factory_ =
std::make_unique<network::TestURLLoaderFactory>();
NavigateAndCommit(GURL(kBeaconPageURL));
PendingBeaconHost::CreateForCurrentDocument(
main_rfh(), test_url_loader_factory_->GetSafeWeakWrapper(),
PendingBeaconService::GetInstance());
return PendingBeaconHost::GetForCurrentDocument(main_rfh());
}
PendingBeaconHost* host() {
return PendingBeaconHost::GetForCurrentDocument(main_rfh());
}
// Ask PendingBeaconHost to create `total` browser-side beacons.
// Returns the mock client beacons that connect to browser-side beacons
// The URLs for the beacons are generated by `CreateBeaconTargetURL()`.
std::vector<MockClientBeacon> CreateBeacons(size_t total,
const std::string& method) {
std::vector<MockClientBeacon> client_beacons(total);
auto* host = CreateHost();
for (size_t i = 0; i < total; i++) {
host->CreateBeacon(client_beacons[i].remote.BindNewPipeAndPassReceiver(),
CreateBeaconTargetURL(i), ToBeaconMethod(method));
}
return client_beacons;
}
std::unique_ptr<MockClientBeacon> CreateBeacon(const std::string& method) {
auto client_beacon = std::make_unique<MockClientBeacon>();
auto* host = CreateHost();
host->CreateBeacon(client_beacon->remote.BindNewPipeAndPassReceiver(),
GURL(kBeaconTargetURL), ToBeaconMethod(method));
return client_beacon;
}
static blink::mojom::BeaconMethod ToBeaconMethod(const std::string& method) {
if (method == net::HttpRequestHeaders::kGetMethod) {
return blink::mojom::BeaconMethod::kGet;
}
return blink::mojom::BeaconMethod::kPost;
}
static GURL CreateBeaconTargetURL(size_t i) {
return GURL(base::StringPrintf("%s/%zu", kBeaconTargetURL, i));
}
// Verifies if the total number of network requests sent via
// `test_url_loader_factory_` equals to `expected`.
void ExpectTotalNetworkRequests(const base::Location& location,
const int expected) {
EXPECT_EQ(test_url_loader_factory_->NumPending(), expected)
<< location.ToString();
}
std::unique_ptr<BrowserContext> CreateBrowserContext() override {
auto context = std::make_unique<TestBrowserContext>();
context->SetPermissionControllerDelegate(
std::make_unique<::testing::NiceMock<MockPermissionManager>>());
return context;
}
// Updates the `permission_type` to the given `permission_status` through
// the MockPermissionManager.
void SetPermissionStatus(blink::PermissionType permission_type,
blink::mojom::PermissionStatus permission_status) {
auto* mock_permission_manager = static_cast<MockPermissionManager*>(
browser_context()->GetPermissionControllerDelegate());
ON_CALL(*mock_permission_manager,
GetPermissionResultForOriginWithoutContext(permission_type,
::testing::_))
.WillByDefault(::testing::Return(PermissionResult(
permission_status, PermissionStatusSource::UNSPECIFIED)));
}
static constexpr char kBeaconTargetURL[] = "/test_send_beacon";
static constexpr char kBeaconPageURL[] = "http://test-pending-beacon";
std::unique_ptr<network::TestURLLoaderFactory> test_url_loader_factory_;
};
class PendingBeaconHostTest : public PendingBeaconHostTestBase {
protected:
void SetUp() override {
const std::vector<base::test::ScopedFeatureList::FeatureAndParams>
enabled_features = {{blink::features::kPendingBeaconAPI,
{{"send_on_pagehide", "true"}}}};
feature_list_.InitWithFeaturesAndParameters(enabled_features, {});
PendingBeaconHostTestBase::SetUp();
}
// Registers a callback to verify if the most-recent network request's content
// matches the given `method` and `url`.
void SetExpectNetworkRequest(const base::Location& location,
const std::string& method,
const GURL& url) {
test_url_loader_factory_->SetInterceptor(base::BindLambdaForTesting(
[location, method, url](const network::ResourceRequest& request) {
EXPECT_EQ(request.mode, network::mojom::RequestMode::kCors);
EXPECT_EQ(request.request_initiator,
url::Origin::Create(GURL(kBeaconPageURL)));
EXPECT_EQ(request.credentials_mode,
network::mojom::CredentialsMode::kSameOrigin);
EXPECT_EQ(request.method, method) << location.ToString();
EXPECT_EQ(request.url, url) << location.ToString();
if (method == net::HttpRequestHeaders::kPostMethod) {
EXPECT_TRUE(request.keepalive) << location.ToString();
}
}));
}
private:
base::test::ScopedFeatureList feature_list_;
};
INSTANTIATE_TEST_SUITE_P(
All,
PendingBeaconHostTest,
testing::ValuesIn<std::vector<std::string>>(
{net::HttpRequestHeaders::kGetMethod,
net::HttpRequestHeaders::kPostMethod}),
[](const testing::TestParamInfo<PendingBeaconHostTest::ParamType>& info) {
return info.param;
});
TEST_P(PendingBeaconHostTest, SendBeacon) {
const std::string method = GetParam();
const auto url = GURL(kBeaconTargetURL);
auto beacon = CreateBeacon(method);
auto& remote = beacon->remote;
SetExpectNetworkRequest(FROM_HERE, method, url);
remote->SendNow();
ExpectTotalNetworkRequests(FROM_HERE, 1);
}
TEST_P(PendingBeaconHostTest, SendOneOfBeacons) {
const std::string method = GetParam();
const size_t total = 5;
// Sends out only the 3rd of 5 created beacons.
auto beacons = CreateBeacons(total, method);
const size_t sent_beacon_i = 2;
SetExpectNetworkRequest(FROM_HERE, method,
CreateBeaconTargetURL(sent_beacon_i));
beacons[sent_beacon_i].remote->SendNow();
ExpectTotalNetworkRequests(FROM_HERE, 1);
}
TEST_P(PendingBeaconHostTest, SendBeacons) {
const std::string method = GetParam();
const size_t total = 5;
// Sends out all 5 created beacons, in reversed order.
auto beacons = CreateBeacons(total, method);
for (int i = beacons.size() - 1; i >= 0; i--) {
SetExpectNetworkRequest(FROM_HERE, method, CreateBeaconTargetURL(i));
beacons[i].remote->SendNow();
}
ExpectTotalNetworkRequests(FROM_HERE, total);
}
TEST_P(PendingBeaconHostTest, DeleteAndSendBeacon) {
const std::string method = GetParam();
const auto url = GURL(kBeaconTargetURL);
auto beacon = CreateBeacon(method);
auto& remote = beacon->remote;
// Deleted beacon won't be sent out by host.
remote->Deactivate();
remote->SendNow();
ExpectTotalNetworkRequests(FROM_HERE, 0);
}
TEST_P(PendingBeaconHostTest, DeleteOneAndSendOtherBeacons) {
const std::string method = GetParam();
const size_t total = 5;
// Creates 5 beacons. Deletes the 3rd of them, and sends out the others.
auto beacons = CreateBeacons(total, method);
const size_t deleted_beacon_i = 2;
beacons[deleted_beacon_i].remote->Deactivate();
for (int i = beacons.size() - 1; i >= 0; i--) {
if (i != deleted_beacon_i) {
SetExpectNetworkRequest(FROM_HERE, method, CreateBeaconTargetURL(i));
}
beacons[i].remote->SendNow();
}
ExpectTotalNetworkRequests(FROM_HERE, total - 1);
}
TEST_P(PendingBeaconHostTest, SendOnDocumentUnloadWithBackgroundSync) {
const std::string method = GetParam();
const size_t total = 5;
// Creates 5 beacons on the page.
auto beacons = CreateBeacons(total, method);
SetPermissionStatus(blink::PermissionType::BACKGROUND_SYNC,
blink::mojom::PermissionStatus::GRANTED);
// Forces deleting the page where `host` resides.
DeleteContents();
ExpectTotalNetworkRequests(FROM_HERE, total);
}
TEST_P(PendingBeaconHostTest,
DoesNotSendOnDocumentUnloadWithoutBackgroundSync) {
const std::string method = GetParam();
const size_t total = 5;
// Creates 5 beacons on the page.
auto beacons = CreateBeacons(total, method);
SetPermissionStatus(blink::PermissionType::BACKGROUND_SYNC,
blink::mojom::PermissionStatus::ASK);
// Forces deleting the page where `host` resides.
DeleteContents();
ExpectTotalNetworkRequests(FROM_HERE, 0);
}
TEST_P(PendingBeaconHostTest, SendOnNavigation) {
const std::string method = GetParam();
const size_t total = 5;
// Creates 5 beacons on the page.
auto beacons = CreateBeacons(total, method);
// Simulates sends on pagehide.
host()->SendAllOnNavigation();
ExpectTotalNetworkRequests(FROM_HERE, total);
}
class BeaconTestBase : public PendingBeaconHostTestBase {
protected:
void TearDown() override {
host_ = nullptr;
PendingBeaconHostTestBase::TearDown();
}
scoped_refptr<network::ResourceRequestBody> CreateRequestBody(
const std::string& data) {
return network::ResourceRequestBody::CreateFromBytes(data.data(),
data.size());
}
scoped_refptr<network::ResourceRequestBody> CreateFileRequestBody(
uint64_t offset = 0,
uint64_t length = 10) {
scoped_refptr<network::ResourceRequestBody> body =
base::MakeRefCounted<network::ResourceRequestBody>();
body->AppendFileRange(base::FilePath(FILE_PATH_LITERAL("file.txt")), offset,
length, base::Time());
return body;
}
scoped_refptr<network::ResourceRequestBody> CreateComplexRequestBody() {
auto body = CreateRequestBody("part1");
body->AppendFileRange(base::FilePath(FILE_PATH_LITERAL("part2.txt")), 0, 10,
base::Time());
return body;
}
scoped_refptr<network::ResourceRequestBody> CreateStreamingRequestBody() {
mojo::PendingRemote<network::mojom::ChunkedDataPipeGetter> remote;
auto unused_receiver = remote.InitWithNewPipeAndPassReceiver();
scoped_refptr<network::ResourceRequestBody> body =
base::MakeRefCounted<network::ResourceRequestBody>();
body->SetToChunkedDataPipe(
std::move(remote), network::ResourceRequestBody::ReadOnlyOnce(false));
return body;
}
mojo::Remote<blink::mojom::PendingBeacon>& CreateBeaconAndPassRemote(
const std::string& method) {
beacon_ = CreateBeacon(method);
return beacon_->remote;
}
private:
// Owned by `main_rfh()`.
PendingBeaconHost* host_;
std::unique_ptr<MockClientBeacon> beacon_;
};
using GetBeaconTest = BeaconTestBase;
TEST_F(GetBeaconTest, AttemptToSetRequestDataForGetBeaconAndTerminated) {
auto& beacon_remote =
CreateBeaconAndPassRemote(net::HttpRequestHeaders::kGetMethod);
// Intercepts Mojo bad-message error.
std::string bad_message;
mojo::SetDefaultProcessErrorHandler(
base::BindLambdaForTesting([&](const std::string& error) {
ASSERT_TRUE(bad_message.empty());
bad_message = error;
}));
beacon_remote->SetRequestData(CreateRequestBody("data"), "");
beacon_remote.FlushForTesting();
EXPECT_EQ(bad_message, "Unexpected BeaconMethod from renderer");
}
using PostBeaconTest = BeaconTestBase;
TEST_F(PostBeaconTest, AttemptToSetRequestDataWithComplexBodyAndTerminated) {
auto& beacon_remote =
CreateBeaconAndPassRemote(net::HttpRequestHeaders::kPostMethod);
// Intercepts Mojo bad-message error.
std::string bad_message;
mojo::SetDefaultProcessErrorHandler(
base::BindLambdaForTesting([&](const std::string& error) {
ASSERT_TRUE(bad_message.empty());
bad_message = error;
}));
beacon_remote->SetRequestData(CreateComplexRequestBody(), "");
beacon_remote.FlushForTesting();
EXPECT_EQ(bad_message, "Complex body is not supported yet");
}
TEST_F(PostBeaconTest, AttemptToSetRequestDataWithStreamingBodyAndTerminated) {
auto& beacon_remote =
CreateBeaconAndPassRemote(net::HttpRequestHeaders::kPostMethod);
// Intercepts Mojo bad-message error.
std::string bad_message;
mojo::SetDefaultProcessErrorHandler(
base::BindLambdaForTesting([&](const std::string& error) {
ASSERT_TRUE(bad_message.empty());
bad_message = error;
}));
beacon_remote->SetRequestData(CreateStreamingRequestBody(), "");
beacon_remote.FlushForTesting();
EXPECT_EQ(bad_message, "Streaming body is not supported.");
}
TEST_F(PostBeaconTest, AttemptToSetRequestURLForPostBeaconAndTerminated) {
auto& beacon_remote =
CreateBeaconAndPassRemote(net::HttpRequestHeaders::kPostMethod);
// Intercepts Mojo bad-message error.
std::string bad_message;
mojo::SetDefaultProcessErrorHandler(
base::BindLambdaForTesting([&](const std::string& error) {
ASSERT_TRUE(bad_message.empty());
bad_message = error;
}));
beacon_remote->SetRequestURL(GURL("/test_set_url"));
beacon_remote.FlushForTesting();
EXPECT_EQ(bad_message, "Unexpected BeaconMethod from renderer");
}
class PostBeaconRequestDataTest : public BeaconTestBase {
protected:
// Registers a callback to verify if the most-recent network request's content
// matches the given `expected_body` and `expected_content_type`.
void SetExpectNetworkRequest(
const base::Location& location,
scoped_refptr<network::ResourceRequestBody> expected_body,
const absl::optional<std::string>& expected_content_type =
absl::nullopt) {
test_url_loader_factory_->SetInterceptor(base::BindLambdaForTesting(
[location, expected_body,
expected_content_type](const network::ResourceRequest& request) {
ASSERT_EQ(request.method, net::HttpRequestHeaders::kPostMethod)
<< location.ToString();
ASSERT_EQ(request.request_body->elements()->size(), 1u)
<< location.ToString();
const auto& expected_element = expected_body->elements()->at(0);
const auto& element = request.request_body->elements()->at(0);
EXPECT_EQ(element.type(), expected_element.type());
if (expected_element.type() == network::DataElement::Tag::kBytes) {
const auto& expected_bytes =
expected_element.As<network::DataElementBytes>();
const auto& bytes = element.As<network::DataElementBytes>();
EXPECT_EQ(bytes.AsStringPiece(), expected_bytes.AsStringPiece())
<< location.ToString();
} else if (expected_element.type() ==
network::DataElement::Tag::kFile) {
const auto& expected_file =
expected_element.As<network::DataElementFile>();
const auto& file = element.As<network::DataElementFile>();
EXPECT_EQ(file.path(), expected_file.path()) << location.ToString();
EXPECT_EQ(file.offset(), expected_file.offset())
<< location.ToString();
EXPECT_EQ(file.length(), expected_file.length())
<< location.ToString();
}
if (!expected_content_type.has_value()) {
EXPECT_FALSE(request.headers.HasHeader(
net::HttpRequestHeaders::kContentType))
<< location.ToString();
return;
}
std::string content_type;
EXPECT_TRUE(request.headers.GetHeader(
net::HttpRequestHeaders::kContentType, &content_type))
<< location.ToString();
EXPECT_EQ(content_type, expected_content_type) << location.ToString();
}));
}
mojo::Remote<blink::mojom::PendingBeacon>& CreateBeaconAndPassRemote() {
return BeaconTestBase::CreateBeaconAndPassRemote(
net::HttpRequestHeaders::kPostMethod);
}
};
TEST_F(PostBeaconRequestDataTest, SendBytesWithCorsSafelistedContentType) {
auto& beacon_remote = CreateBeaconAndPassRemote();
auto body = CreateRequestBody("data");
beacon_remote->SetRequestData(body, "text/plain");
SetExpectNetworkRequest(FROM_HERE, body, "text/plain");
beacon_remote->SendNow();
ExpectTotalNetworkRequests(FROM_HERE, 1);
}
TEST_F(PostBeaconRequestDataTest, SendBytesWithEmptyContentType) {
auto& beacon_remote = CreateBeaconAndPassRemote();
auto body = CreateRequestBody("data");
beacon_remote->SetRequestData(body, "");
SetExpectNetworkRequest(FROM_HERE, body);
beacon_remote->SendNow();
ExpectTotalNetworkRequests(FROM_HERE, 1);
}
TEST_F(PostBeaconRequestDataTest, SendBlobWithCorsSafelistedContentType) {
auto& beacon_remote = CreateBeaconAndPassRemote();
auto body = CreateFileRequestBody();
beacon_remote->SetRequestData(body, "text/plain");
SetExpectNetworkRequest(FROM_HERE, body, "text/plain");
beacon_remote->SendNow();
ExpectTotalNetworkRequests(FROM_HERE, 1);
}
TEST_F(PostBeaconRequestDataTest, SendBlobWithEmptyContentType) {
auto& beacon_remote = CreateBeaconAndPassRemote();
auto body = CreateFileRequestBody();
beacon_remote->SetRequestData(body, "");
SetExpectNetworkRequest(FROM_HERE, body);
beacon_remote->SendNow();
ExpectTotalNetworkRequests(FROM_HERE, 1);
}
TEST_F(PostBeaconRequestDataTest, SendBlobWithNonCorsSafelistedContentType) {
auto& beacon_remote = CreateBeaconAndPassRemote();
auto body = CreateFileRequestBody();
beacon_remote->SetRequestData(body, "application/unsafe");
SetExpectNetworkRequest(FROM_HERE, body, "application/unsafe");
beacon_remote->SendNow();
ExpectTotalNetworkRequests(FROM_HERE, 1);
}
} // namespace content