blob: 719e099950fabff3568c5fca6d40e349eaf9e446 [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "services/network/prefetch_url_loader_client.h"
#include <stdint.h>
#include <algorithm>
#include <type_traits>
#include <utility>
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/task/thread_pool.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "mojo/public/cpp/system/data_pipe.h"
#include "mojo/public/cpp/system/data_pipe_utils.h"
#include "net/base/isolation_info.h"
#include "net/base/network_isolation_key.h"
#include "net/cookies/site_for_cookies.h"
#include "net/http/http_response_headers.h"
#include "net/http/http_status_code.h"
#include "net/url_request/redirect_info.h"
#include "net/url_request/referrer_policy.h"
#include "services/network/prefetch_cache.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/test/mock_url_loader_client.h"
#include "services/network/test/test_utils.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
#include "url/origin.h"
namespace network {
namespace {
using ::testing::_;
using ::testing::Eq;
using ::testing::InSequence;
using ::testing::IsTrue;
using ::testing::MockFunction;
using ::testing::NotNull;
using ::testing::Optional;
using ::testing::Pointee;
using ::testing::StrictMock;
using ::testing::WithArg;
constexpr uint64_t kTestCurrentPosition = 53;
constexpr uint64_t kTestTotalSize = 103;
constexpr int32_t kTransferSizeDiff = 27;
constexpr size_t kDataPipeCapacity = 4096;
constexpr char kResponseBody[] = "Some fairly unique data";
constexpr size_t kBigBufferSize = 1 << 16;
// Returns a URL for our tests to use. The actual value isn't important.
GURL TestURL() {
return GURL("https://origin.example/i.js");
}
// Returns an origin matching our test URL.
url::Origin TestOrigin() {
return url::Origin::Create(TestURL());
}
// Returns an IsolationInfo object matching our test URL.
net::IsolationInfo TestIsolationInfo() {
return net::IsolationInfo::Create(
net::IsolationInfo::RequestType::kMainFrame, TestOrigin(), TestOrigin(),
net::SiteForCookies::FromOrigin(TestOrigin()));
}
// Returns a NetworkIsolationKey object matching our test URL.
net::NetworkIsolationKey TestNIK() {
const net::NetworkIsolationKey nik =
TestIsolationInfo().network_isolation_key();
return nik;
}
// Returns a ResourceRequest matching our test URL.
ResourceRequest TestRequest() {
ResourceRequest request;
request.url = TestURL();
request.trusted_params.emplace();
request.trusted_params->isolation_info = TestIsolationInfo();
return request;
}
// Returns a dummy value for EarlyHints.
mojom::EarlyHintsPtr TestEarlyHints() {
auto early_hints = mojom::EarlyHints::New();
early_hints->headers = mojom::ParsedHeaders::New();
return early_hints;
}
// Returns a successfully URLResponseHead.
mojom::URLResponseHeadPtr TestURLResponseHead() {
return CreateURLResponseHead(net::HTTP_OK);
}
// A GMock matcher for the URLResponseHead returned by TestURLResponseHead().
MATCHER(URLResponseHeadIsOk, "URLResponseHead is ok") {
return arg->headers->response_code() == net::HTTP_OK;
}
// Returns a ScopedDataPipeConsumerHandle for use in tests. Reading from the
// pipe will give `kResponseBody`.
mojo::ScopedDataPipeConsumerHandle TestDataPipeConsumer() {
mojo::ScopedDataPipeProducerHandle producer;
mojo::ScopedDataPipeConsumerHandle consumer;
mojo::CreateDataPipe(kDataPipeCapacity, producer, consumer);
// Write to the data pipe from another thread to make sure we don't block.
base::ThreadPool::PostTask(
FROM_HERE, base::MayBlock(),
base::BindOnce(
[](mojo::ScopedDataPipeProducerHandle destination) {
mojo::BlockingCopyFromString(kResponseBody, destination);
// `destination` will be closed automatically on leaving scope.
},
std::move(producer)));
return consumer;
}
// A function which will cause the test to fail if `consumer` is not a data pipe
// that yields kResponseBody. Blocks until the data pipe is completely read.
void CheckDataPipeContents(mojo::ScopedDataPipeConsumerHandle consumer) {
std::string contents;
mojo::BlockingCopyToString(std::move(consumer), &contents);
EXPECT_EQ(contents, kResponseBody);
}
// Creates a BigBuffer suitable for use in tests. The contents are sufficiently
// unique that there shouldn't be an accidental match.
mojo_base::BigBuffer TestBigBuffer() {
std::vector<uint8_t> contents;
contents.reserve(kBigBufferSize);
uint8_t value = 1;
for (size_t i = 0; i < kBigBufferSize; ++i) {
contents.push_back(value);
value = (value * 7 + 1) % 256;
}
return mojo_base::BigBuffer(contents);
}
// Verifies that a BigBuffer object matches the one created by TestBigBuffer().
MATCHER(BigBufferHasExpectedContents,
"does the BigBuffer have the right contents") {
auto expected = TestBigBuffer();
return std::ranges::equal(base::span(expected), base::span(arg));
}
// Returns a RedirectInfo object that is useful for use in tests. It is
// same-origin with the URL returned by TestURL().
net::RedirectInfo TestRedirectInfo() {
using net::RedirectInfo;
constexpr char kRedirectTo[] = "https://origin.example/resources/i.js";
return RedirectInfo::ComputeRedirectInfo(
"GET", TestURL(), TestIsolationInfo().site_for_cookies(),
RedirectInfo::FirstPartyURLPolicy::NEVER_CHANGE_URL,
net::ReferrerPolicy::CLEAR_ON_TRANSITION_FROM_SECURE_TO_INSECURE,
TestOrigin().Serialize(), /*original_initiator=*/std::nullopt, 301,
GURL(kRedirectTo), std::nullopt, false);
}
// Returns true if two SiteForCookies objects match.
bool Equals(const net::SiteForCookies& lhs, const net::SiteForCookies& rhs) {
return lhs.IsEquivalent(rhs);
}
// Returns true if a RedirectInfo object matches the one created by
// TestRedirectInfo().
MATCHER(EqualsTestRedirectInfo, "equals test RedirectInfo") {
const net::RedirectInfo expected = TestRedirectInfo();
return arg.status_code == expected.status_code &&
arg.new_method == expected.new_method &&
arg.new_url == expected.new_url &&
Equals(arg.new_site_for_cookies, expected.new_site_for_cookies) &&
arg.new_referrer == expected.new_referrer &&
arg.insecure_scheme_was_upgraded ==
expected.insecure_scheme_was_upgraded &&
arg.is_signed_exchange_fallback_redirect ==
expected.is_signed_exchange_fallback_redirect &&
arg.new_referrer_policy == expected.new_referrer_policy &&
arg.critical_ch_restart_time == expected.critical_ch_restart_time;
}
// Returns a successful URLLoaderCompletionStatus.
URLLoaderCompletionStatus TestURLLoaderCompletionStatus() {
return URLLoaderCompletionStatus(net::OK);
}
// A GMock matcher that Verifies that a URLLoaderCompletionStatus matches the
// one returned by TestURLLoaderCompletionStatus().
MATCHER(URLLoaderCompletionStatusIsOk, "URLLoaderCompletionStatus is ok") {
const URLLoaderCompletionStatus expected = TestURLLoaderCompletionStatus();
const URLLoaderCompletionStatus& actual = arg;
using S = URLLoaderCompletionStatus;
auto equals = [&](auto member_ptr) {
return expected.*member_ptr == actual.*member_ptr;
};
// `completion_time` is intentionally omitted as it is different every time.
// `ssl_info` is omitted as it lacks an equality operator and it's not worth
// implementing one just for this test.
return equals(&S::error_code) && equals(&S::extended_error_code) &&
equals(&S::exists_in_cache) && equals(&S::encoded_data_length) &&
equals(&S::encoded_body_length) && equals(&S::decoded_body_length) &&
equals(&S::cors_error_status) &&
equals(&S::private_network_access_preflight_result) &&
equals(&S::trust_token_operation_status) &&
equals(&S::blocked_by_response_reason) &&
equals(&S::should_report_orb_blocking) &&
equals(&S::resolve_error_info) &&
equals(&S::should_collapse_initiator);
}
// An implementation of URLLoader that does nothing.
class StubURLLoader final : public network::mojom::URLLoader {
public:
explicit StubURLLoader(
mojo::PendingReceiver<network::mojom::URLLoader> url_loader_receiver)
: receiver_(this, std::move(url_loader_receiver)) {}
// network::mojom::URLLoader overrides.
void FollowRedirect(
const std::vector<std::string>& removed_headers,
const net::HttpRequestHeaders& modified_headers,
const net::HttpRequestHeaders& modified_cors_exempt_headers,
const std::optional<GURL>& new_url) override {}
void SetPriority(net::RequestPriority priority,
int32_t intra_priority_value) override {}
private:
mojo::Receiver<network::mojom::URLLoader> receiver_;
};
class PrefetchURLLoaderClientTest : public ::testing::Test {
protected:
PrefetchURLLoaderClientTest()
: client_(cache()->Emplace(TestRequest())),
stub_url_loader_(client_->GetURLLoaderPendingReceiver()),
client_pending_remote_(client_->BindNewPipeAndPassRemote()),
mock_client_receiver_(&mock_client_) {}
~PrefetchURLLoaderClientTest() override {
// Avoid calls to a disconnect handler on the stack.
mock_client_receiver_.reset();
}
// The cache that owns the object under test.
PrefetchCache* cache() { return &cache_; }
// The object under test.
PrefetchURLLoaderClient* client() { return client_; }
// A mock URLLoaderClient that will stand-in for the URLLoaderClient inside
// the render process.
StrictMock<MockURLLoaderClient>& mock_client() { return mock_client_; }
// Arranges for `disconnect_handler` to be called when `mock_client_receiver_`
// detects a disconnection. Must be called after `Consume()`.
void set_disconnect_handler(MockFunction<void()>* disconnect_handler) {
// This use of base::Unretained is potentially unsafe as
// `disconnect_handler` is on the stack. It is mitigated by resetting
// `mock_client_receiver_` in the destructor.
mock_client_receiver_.set_disconnect_handler(base::BindOnce(
&MockFunction<void()>::Call, base::Unretained(disconnect_handler)));
}
// Pretend the datapipe that passed mojo messages from the "real" URLLoader
// disconnected.
void ResetClientPendingRemote() { client_pending_remote_.reset(); }
// Remove the reference to the client. This needs to be called by tests which
// arrange for the client to be deleted before the test ends, in order to
// prevent triggering the dangling pointer detection.
void ClearClientPointer() { client_ = nullptr; }
// Sets `client_` to nullptr and returns the old value. This is needed for
// tests that need a call a method on `client_` that will delete it.
PrefetchURLLoaderClient* TakeClientPointer() {
return std::exchange(client_, nullptr);
}
// Calls Consume() on `client_`, hooking it up to the mock client.
void Consume() {
client_->Consume(loader_.BindNewPipeAndPassReceiver(),
mock_client_receiver_.BindNewPipeAndPassRemote());
EXPECT_TRUE(loader_.is_bound());
}
// Calls all the mojo methods on `client_` in order with verifiable
// parameters. This doesn't in any way correspond to the real behaviour of a
// URLLoader.
void CallAllMojoMethods() {
client_->OnReceiveEarlyHints(TestEarlyHints());
client_->OnReceiveResponse(TestURLResponseHead(), TestDataPipeConsumer(),
TestBigBuffer());
client_->OnReceiveRedirect(TestRedirectInfo(), TestURLResponseHead());
client_->OnUploadProgress(kTestCurrentPosition, kTestTotalSize,
base::DoNothing());
client_->OnTransferSizeUpdated(kTransferSizeDiff);
client_->OnComplete(TestURLLoaderCompletionStatus());
}
// This adds expectations that all the methods on `client` will be called with
// arguments matching those in `CallAllMojoMethods()`.
void ExpectCallMojoMethods() {
EXPECT_CALL(mock_client_, OnReceiveEarlyHints(IsTrue()));
EXPECT_CALL(mock_client_,
OnReceiveResponse(URLResponseHeadIsOk(), _,
Optional(BigBufferHasExpectedContents())))
.WillOnce(WithArg<1>(CheckDataPipeContents));
EXPECT_CALL(mock_client_, OnReceiveRedirect(EqualsTestRedirectInfo(),
URLResponseHeadIsOk()));
EXPECT_CALL(mock_client_, OnUploadProgress(Eq(kTestCurrentPosition),
Eq(kTestTotalSize), _))
.WillOnce(
WithArg<2>([](auto&& callback) { std::move(callback).Run(); }));
EXPECT_CALL(mock_client_, OnTransferSizeUpdated(Eq(kTransferSizeDiff)));
EXPECT_CALL(mock_client_, OnComplete(URLLoaderCompletionStatusIsOk()));
}
void RunUntilIdle() { task_environment_.RunUntilIdle(); }
private:
base::test::TaskEnvironment task_environment_;
// A dummy URLLoader that adopts `client_`'s url_loader_ when Consume() is
// called.
mojo::Remote<mojom::URLLoader> loader_;
// Owner for `client_`.
PrefetchCache cache_;
// The client under test. Owned by `cache_`.
raw_ptr<PrefetchURLLoaderClient> client_;
// The "real" URLLoader that `client_` connects to.
StubURLLoader stub_url_loader_;
// Connected to `client_`. Used to detect when `client_` is destroyed. In
// normal operation this would be passed to a real URLLoader.
mojo::PendingRemote<mojom::URLLoaderClient> client_pending_remote_;
// Represents the renderer-side client that will eventually consume this
// object.
StrictMock<MockURLLoaderClient> mock_client_;
// Forwards mojo calls to `mock_client_`.
mojo::Receiver<mojom::URLLoaderClient> mock_client_receiver_;
};
// Tests that call Consume() leak memory and hit an assert(), so they are
// disabled on address sanitizer and debug builds.
// TODO(crbug.com/375425822): Fix the leak before running this code in
// production.
#if !defined(NDEBUG) || defined(ADDRESS_SANITIZER)
#define MAYBE_RecordAndReplay DISABLED_RecordAndReplay
#define MAYBE_DelegateDirectly DISABLED_DelegateDirectly
#define MAYBE_ReplayAfterResponse DISABLED_ReplayAfterResponse
#define MAYBE_ConsumeRemovesFromCache DISABLED_ConsumeRemovesFromCache
#else
#define MAYBE_RecordAndReplay RecordAndReplay
#define MAYBE_DelegateDirectly DelegateDirectly
#define MAYBE_ReplayAfterResponse ReplayAfterResponse
#define MAYBE_ConsumeRemovesFromCache ConsumeRemovesFromCache
#endif
TEST_F(PrefetchURLLoaderClientTest, Construct) {
EXPECT_EQ(client()->url(), TestURL());
EXPECT_EQ(client()->network_isolation_key(), TestNIK());
EXPECT_GT(client()->expiry_time(), base::TimeTicks::Now());
EXPECT_LT(client()->expiry_time(), base::TimeTicks::Now() + base::Days(1));
}
TEST_F(PrefetchURLLoaderClientTest, MAYBE_RecordAndReplay) {
MockFunction<void()> checkpoint;
MockFunction<void()> disconnect;
{
InSequence s;
EXPECT_CALL(checkpoint, Call());
ExpectCallMojoMethods();
EXPECT_CALL(disconnect, Call());
}
CallAllMojoMethods();
ResetClientPendingRemote();
checkpoint.Call();
Consume();
set_disconnect_handler(&disconnect);
ClearClientPointer();
RunUntilIdle();
}
// The difference from the previous test is that now Consume() is called
// before any of the delegating methods, so there's no need to record them.
TEST_F(PrefetchURLLoaderClientTest, MAYBE_DelegateDirectly) {
MockFunction<void()> checkpoint;
MockFunction<void()> disconnect;
{
InSequence s;
ExpectCallMojoMethods();
EXPECT_CALL(disconnect, Call());
EXPECT_CALL(checkpoint, Call());
}
Consume();
set_disconnect_handler(&disconnect);
CallAllMojoMethods();
ClearClientPointer();
ResetClientPendingRemote();
RunUntilIdle();
checkpoint.Call();
}
// This test just verifies that all the recorded callbacks can be destroyed
// without leaks.
TEST_F(PrefetchURLLoaderClientTest, RecordAndDiscard) {
CallAllMojoMethods();
RunUntilIdle();
}
// Verifies that setting the client after the response comes but before it
// completes works.
TEST_F(PrefetchURLLoaderClientTest, MAYBE_ReplayAfterResponse) {
MockFunction<void(int)> checkpoint;
MockFunction<void()> disconnect;
{
InSequence s;
EXPECT_CALL(checkpoint, Call(0));
EXPECT_CALL(mock_client(),
OnReceiveResponse(URLResponseHeadIsOk(), _,
Optional(BigBufferHasExpectedContents())))
.WillOnce(WithArg<1>(CheckDataPipeContents));
EXPECT_CALL(checkpoint, Call(1));
EXPECT_CALL(mock_client(), OnComplete(URLLoaderCompletionStatusIsOk()));
EXPECT_CALL(checkpoint, Call(2));
EXPECT_CALL(disconnect, Call());
EXPECT_CALL(checkpoint, Call(3));
}
client()->OnReceiveResponse(TestURLResponseHead(), TestDataPipeConsumer(),
TestBigBuffer());
RunUntilIdle();
checkpoint.Call(0);
Consume();
set_disconnect_handler(&disconnect);
RunUntilIdle();
checkpoint.Call(1);
client()->OnComplete(TestURLLoaderCompletionStatus());
RunUntilIdle();
checkpoint.Call(2);
ClearClientPointer();
ResetClientPendingRemote();
RunUntilIdle();
checkpoint.Call(3);
}
TEST_F(PrefetchURLLoaderClientTest, MAYBE_ConsumeRemovesFromCache) {
Consume();
EXPECT_FALSE(
cache()->Lookup(client()->network_isolation_key(), client()->url()));
}
TEST_F(PrefetchURLLoaderClientTest, BadResponseCode) {
constexpr auto kBadResponseCode = net::HTTP_NOT_FOUND;
TakeClientPointer()->OnReceiveResponse(
CreateURLResponseHead(kBadResponseCode), TestDataPipeConsumer(),
TestBigBuffer());
// It should have been deleted from the cache.
EXPECT_FALSE(cache()->Lookup(TestNIK(), TestURL()));
}
TEST_F(PrefetchURLLoaderClientTest, BadHeader) {
auto url_response_head = TestURLResponseHead();
url_response_head->headers->AddHeader("Vary", "Sec-Purpose, Set-Cookie");
TakeClientPointer()->OnReceiveResponse(
std::move(url_response_head), TestDataPipeConsumer(), TestBigBuffer());
// It should have been deleted from the cache.
EXPECT_FALSE(cache()->Lookup(TestNIK(), TestURL()));
}
TEST_F(PrefetchURLLoaderClientTest, NoStore) {
auto url_response_head = TestURLResponseHead();
url_response_head->headers->AddHeader("Cache-Control", "no-cache, no-store");
TakeClientPointer()->OnReceiveResponse(
std::move(url_response_head), TestDataPipeConsumer(), TestBigBuffer());
// It should have been deleted from the cache.
EXPECT_FALSE(cache()->Lookup(TestNIK(), TestURL()));
}
} // namespace
} // namespace network