blob: 4f2dd1c6d8863b33f6413f487eb266aaf2998825 [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 <tuple>
#include <vector>
#include "base/files/file_path.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/strings/strcat.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/child_process_termination_info.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 {
namespace {
constexpr char kBeaconTargetURL[] = "https://pending-beacon.test/send";
constexpr char kBeaconPageURL[] = "https://pending-beacon.test";
MATCHER_P2(VerifyResourceRequest,
method,
url,
base::StrCat({"ResourceRequest is", negation ? " not" : "",
" matched"})) {
const network::ResourceRequest& req = arg;
if (req.mode != network::mojom::RequestMode::kCors) {
*result_listener << "request mode must be CORS";
return false;
}
if (req.request_initiator != url::Origin::Create(GURL(kBeaconPageURL))) {
*result_listener << "request initiator must be " << kBeaconPageURL;
return false;
}
if (req.credentials_mode != network::mojom::CredentialsMode::kSameOrigin) {
*result_listener << "credentials mode must be Same-Origin";
return false;
}
if (req.method != method) {
return false;
}
if (req.url != url) {
*result_listener << "expect url: " << url << ", got: " << req.url;
return false;
}
if (method == net::HttpRequestHeaders::kPostMethod) {
if (!req.keepalive) {
*result_listener << "Post request must set keepalive";
return false;
}
}
return true;
}
} // namespace
struct MockClientBeacon {
MockClientBeacon(const MockClientBeacon&) = delete;
MockClientBeacon& operator=(const MockClientBeacon&) = delete;
MockClientBeacon() = default;
void SendNow() {
remote->SendNow();
remote.FlushForTesting();
}
mojo::Remote<blink::mojom::PendingBeacon> remote;
};
class PendingBeaconHostTestBase : public RenderViewHostTestHarness {
public:
PendingBeaconHostTestBase(const PendingBeaconHostTestBase&) = delete;
PendingBeaconHostTestBase& operator=(const PendingBeaconHostTestBase&) =
delete;
PendingBeaconHostTestBase() = default;
void TearDown() override {
// Clean up error handler, to avoid causing other tests run in the same
// process from crashing.
mojo::SetDefaultProcessErrorHandler(base::NullCallback());
RenderViewHostTestHarness::TearDown();
}
protected:
PendingBeaconHost* host() { return GetOrCreateHostIfNotExist(); }
mojo::Remote<blink::mojom::PendingBeaconHost>& host_remote() {
DCHECK(GetOrCreateHostIfNotExist());
return host_remote_;
}
// 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) {
GetOrCreateHostIfNotExist();
std::vector<MockClientBeacon> client_beacons(total);
for (size_t i = 0; i < total; i++) {
host_remote_->CreateBeacon(
client_beacons[i].remote.BindNewPipeAndPassReceiver(),
CreateBeaconTargetURL(i), ToBeaconMethod(method));
}
host_remote_.FlushForTesting();
return client_beacons;
}
std::unique_ptr<MockClientBeacon> CreateBeacon(const std::string& method) {
return CreateBeacon(method, kBeaconTargetURL);
}
std::unique_ptr<MockClientBeacon> CreateBeacon(const std::string& method,
const std::string& url) {
GetOrCreateHostIfNotExist();
auto client_beacon = std::make_unique<MockClientBeacon>();
host_remote_->CreateBeacon(
client_beacon->remote.BindNewPipeAndPassReceiver(), GURL(url),
ToBeaconMethod(method));
host_remote_.FlushForTesting();
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::_, ::testing::_))
.WillByDefault(::testing::Return(PermissionResult(
permission_status, PermissionStatusSource::UNSPECIFIED)));
}
std::unique_ptr<network::TestURLLoaderFactory> test_url_loader_factory_;
private:
// Returns an instance of PendingBeaconHost. Creates one if it does not exist.
// The returned PendingBeaconHost 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* GetOrCreateHostIfNotExist() {
if (auto* host = PendingBeaconHost::GetForCurrentDocument(main_rfh())) {
return host;
}
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());
auto* host = PendingBeaconHost::GetForCurrentDocument(main_rfh());
host->SetReceiver(host_remote_.BindNewPipeAndPassReceiver());
return host;
}
// Binds to the host from `GetOrCreateHostIfNotExist()`.
mojo::Remote<blink::mojom::PendingBeaconHost> host_remote_;
};
class PendingBeaconHostTest
: public PendingBeaconHostTestBase,
public ::testing::WithParamInterface<std::string> {
protected:
void SetUp() override {
const std::vector<base::test::FeatureRefAndParams> 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(
[this, location, method, url](const network::ResourceRequest& request) {
if (has_verified_request_) {
return;
}
has_verified_request_ = true;
EXPECT_THAT(request, VerifyResourceRequest(method, url))
<< location.ToString();
}));
}
private:
base::test::ScopedFeatureList feature_list_;
bool has_verified_request_ = false;
};
INSTANTIATE_TEST_SUITE_P(
All,
PendingBeaconHostTest,
::testing::Values(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);
SetExpectNetworkRequest(FROM_HERE, method, url);
beacon->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].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].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].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, SendOnDocumentUnloadWithoutBackgroundSync) {
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, total);
}
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);
}
TEST_P(PendingBeaconHostTest, SendOnProcessExit) {
const std::string method = GetParam();
const size_t total = 5;
// Creates 5 beacons on the page.
auto beacons = CreateBeacons(total, method);
// Simulates sending on process exits.
ChildProcessTerminationInfo termination_info;
host()->RenderProcessExited(main_rfh()->GetProcess(), termination_info);
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()`.
raw_ptr<PendingBeaconHost> host_;
std::unique_ptr<MockClientBeacon> beacon_;
};
struct BeaconURLTestType {
const char* name;
const char* url_;
bool expect_supported;
std::string url() const { return std::string(url_); }
};
// GURL will try to canonicalize invalid url strings.
constexpr BeaconURLTestType kBeaconURLTestCases[] = {
{"HTTP_LOCALHOST_URL", "http://localhost", false},
{"HTTPS_LOCALHOST_URL", "https://localhost", true},
{"IP_URL", "127.0.0.1", false},
{"HTTP_IP_URL", "http://127.0.0.1", false},
{"HTTPS_IP_URL", "https://127.0.0.1", true},
{"HTTP_URL", "http://example.com", false},
{"HTTPS_URL", "https://example.com", true},
{"FILE_URL", "file://tmp", false},
{"SSH_URL", "ssh://example.com", false},
{"ABOUT_BLANK_URL", "about:blank", false},
{"JAVASCRIPT_URL", "javascript:alert('');", false},
};
class CreateBeaconTest : public BeaconTestBase,
public ::testing::WithParamInterface<
std::tuple<std::string, BeaconURLTestType>> {};
INSTANTIATE_TEST_SUITE_P(
All,
CreateBeaconTest,
::testing::Combine(::testing::Values(net::HttpRequestHeaders::kGetMethod,
net::HttpRequestHeaders::kPostMethod),
::testing::ValuesIn(kBeaconURLTestCases)),
[](const ::testing::TestParamInfo<
std::tuple<std::string, BeaconURLTestType>>& info) {
return base::StrCat(
{std::get<0>(info.param), "_", std::get<1>(info.param).name});
});
TEST_P(CreateBeaconTest, CreateWithURL) {
const std::string& method = std::get<0>(GetParam());
const auto url = std::get<1>(GetParam()).url();
const bool expect_supported = std::get<1>(GetParam()).expect_supported;
// 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;
}));
CreateBeacon(method, url);
if (!expect_supported) {
EXPECT_EQ(bad_message, "Unexpected url format from renderer");
}
}
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");
}
TEST_F(GetBeaconTest, AttemptToSetRequestURLWithInvalidSchemeAndTerminated) {
auto& beacon_remote =
CreateBeaconAndPassRemote(net::HttpRequestHeaders::kGetMethod);
for (const auto& test_case : kBeaconURLTestCases) {
// 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_case.url()));
beacon_remote.FlushForTesting();
if (!test_case.expect_supported) {
EXPECT_EQ(bad_message, "Unexpected url format from renderer")
<< test_case.name;
}
}
}
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