| // 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 |