blob: 221ee9062da9ea4262a4903cf815abf82cfb5d99 [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "extensions/renderer/extension_localization_throttle.h"
#include "base/test/task_environment.h"
#include "extensions/renderer/shared_l10n_map.h"
#include "mojo/public/cpp/bindings/receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "mojo/public/cpp/system/data_pipe_utils.h"
#include "mojo/public/cpp/system/string_data_source.h"
#include "net/base/request_priority.h"
#include "services/network/test/test_url_loader_client.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/loader/url_loader_throttle.h"
#include "third_party/blink/public/platform/web_url.h"
namespace extensions {
namespace {
class FakeURLLoader final : public network::mojom::URLLoader {
public:
enum class Status {
kInitial,
kPauseReading,
kResumeReading,
};
explicit FakeURLLoader(
mojo::PendingReceiver<network::mojom::URLLoader> url_loader_receiver)
: receiver_(this, std::move(url_loader_receiver)) {}
~FakeURLLoader() override = default;
FakeURLLoader(const FakeURLLoader&) = delete;
FakeURLLoader& operator=(const FakeURLLoader&) = delete;
// 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 absl::optional<GURL>& new_url) override {
NOTREACHED();
}
void SetPriority(net::RequestPriority priority,
int32_t intra_priority_value) override {
set_priority_called_ = true;
}
void PauseReadingBodyFromNet() override { status_ = Status::kPauseReading; }
void ResumeReadingBodyFromNet() override { status_ = Status::kResumeReading; }
bool set_priority_called() const { return set_priority_called_; }
Status status() const { return status_; }
private:
bool set_priority_called_ = false;
Status status_ = Status::kInitial;
mojo::Receiver<network::mojom::URLLoader> receiver_;
};
class FakeDelegate : public blink::URLLoaderThrottle::Delegate {
public:
// Implements blink::URLLoaderThrottle::Delegate.
void CancelWithError(int error_code,
base::StringPiece custom_reason) override {
cancel_error_code_ = error_code;
cancel_custom_reason_ = std::string(custom_reason);
}
void Resume() override { NOTREACHED(); }
void SetPriority(net::RequestPriority priority) override { NOTREACHED(); }
void UpdateDeferredResponseHead(
network::mojom::URLResponseHeadPtr new_response_head,
mojo::ScopedDataPipeConsumerHandle body) override {
NOTREACHED();
}
void PauseReadingBodyFromNet() override { NOTREACHED(); }
void ResumeReadingBodyFromNet() override { NOTREACHED(); }
void InterceptResponse(
mojo::PendingRemote<network::mojom::URLLoader> new_loader,
mojo::PendingReceiver<network::mojom::URLLoaderClient>
new_client_receiver,
mojo::PendingRemote<network::mojom::URLLoader>* original_loader,
mojo::PendingReceiver<network::mojom::URLLoaderClient>*
original_client_receiver,
mojo::ScopedDataPipeConsumerHandle* body) override {
is_intercepted_ = true;
destination_loader_remote_.Bind(std::move(new_loader));
ASSERT_TRUE(
mojo::FusePipes(std::move(new_client_receiver),
mojo::PendingRemote<network::mojom::URLLoaderClient>(
destination_loader_client_.CreateRemote())));
source_url_loader_ = std::make_unique<FakeURLLoader>(
original_loader->InitWithNewPipeAndPassReceiver());
*original_client_receiver =
source_loader_client_remote_.BindNewPipeAndPassReceiver();
DCHECK(!source_body_handle_);
mojo::ScopedDataPipeConsumerHandle consumer_handle;
EXPECT_EQ(MOJO_RESULT_OK,
mojo::CreateDataPipe(/*options=*/nullptr, source_body_handle_,
consumer_handle));
body->swap(consumer_handle);
destination_loader_client()->OnReceiveResponse(
network::mojom::URLResponseHead::New(), std::move(consumer_handle),
absl::nullopt);
}
void LoadResponseBody(const std::string& body) {
mojo::BlockingCopyFromString(body, source_body_handle_);
}
void CompleteResponse() {
source_loader_client_remote()->OnComplete(
network::URLLoaderCompletionStatus());
source_body_handle_.reset();
}
bool is_intercepted() const { return is_intercepted_; }
const absl::optional<int>& cancel_error_code() const {
return cancel_error_code_;
}
const absl::optional<std::string>& cancel_custom_reason() const {
return cancel_custom_reason_;
}
mojo::Remote<network::mojom::URLLoader>& destination_loader_remote() {
return destination_loader_remote_;
}
network::TestURLLoaderClient* destination_loader_client() {
return &destination_loader_client_;
}
FakeURLLoader* source_url_loader() { return source_url_loader_.get(); }
mojo::Remote<network::mojom::URLLoaderClient>& source_loader_client_remote() {
return source_loader_client_remote_;
}
mojo::ScopedDataPipeProducerHandle& source_body_handle() {
return source_body_handle_;
}
private:
bool is_intercepted_ = false;
absl::optional<int> cancel_error_code_;
absl::optional<std::string> cancel_custom_reason_;
// The chain of mojom::URLLoaderClient:
// [Blink side]
// destination_loader_client_
// <- ExtensionLocalizationURLLoader::destination_url_loader_client_
// <- ExtensionLocalizationURLLoader
// <- ExtensionLocalizationURLLoader::source_url_client_receiver_
// <- source_loader_client_remote_
// [Browser process side]
// The chain of mojom::URLLoader:
// [Blink side]
// destination_loader_remote_
// -> ExtensionLocalizationURLLoader (SelfOwnedReceiver)
// -> ExtensionLocalizationURLLoader::source_url_loader_
// -> source_url_loader_
// [Browser process side]
mojo::Remote<network::mojom::URLLoader> destination_loader_remote_;
network::TestURLLoaderClient destination_loader_client_;
std::unique_ptr<FakeURLLoader> source_url_loader_;
mojo::Remote<network::mojom::URLLoaderClient> source_loader_client_remote_;
mojo::ScopedDataPipeProducerHandle source_body_handle_;
};
class ExtensionLocalizationThrottleTest : public testing::Test {
protected:
void SetUp() override {
extensions::SharedL10nMap::L10nMessagesMap messages;
messages.insert(std::make_pair("hello", "hola"));
messages.insert(std::make_pair("world", "mundo"));
extensions::SharedL10nMap::GetInstance().SetMessagesForTesting(
"some_id", std::move(messages));
}
// Be the first member so it is destroyed last.
base::test::TaskEnvironment task_environment_;
};
TEST_F(ExtensionLocalizationThrottleTest, DoNotCreate) {
EXPECT_FALSE(ExtensionLocalizationThrottle::MaybeCreate(
blink::WebURL(GURL("https://example.com/test.css"))));
EXPECT_FALSE(ExtensionLocalizationThrottle::MaybeCreate(
blink::WebURL(GURL("http://example.com/test.css"))));
}
TEST_F(ExtensionLocalizationThrottleTest, DoNotIntercept) {
const GURL url("chrome-extension://some_id/test.txt");
auto throttle =
ExtensionLocalizationThrottle::MaybeCreate(blink::WebURL(url));
ASSERT_TRUE(throttle);
auto delegate = std::make_unique<FakeDelegate>();
throttle->set_delegate(delegate.get());
auto response_head = network::mojom::URLResponseHead::New();
response_head->mime_type = "text/plain";
bool defer = false;
throttle->WillProcessResponse(url, response_head.get(), &defer);
EXPECT_FALSE(defer);
EXPECT_FALSE(delegate->is_intercepted());
}
TEST_F(ExtensionLocalizationThrottleTest, OneMessage) {
const GURL url("chrome-extension://some_id/test.css");
auto throttle =
ExtensionLocalizationThrottle::MaybeCreate(blink::WebURL(url));
ASSERT_TRUE(throttle);
auto delegate = std::make_unique<FakeDelegate>();
throttle->set_delegate(delegate.get());
auto response_head = network::mojom::URLResponseHead::New();
response_head->mime_type = "text/css";
bool defer = false;
throttle->WillProcessResponse(url, response_head.get(), &defer);
EXPECT_FALSE(defer);
EXPECT_TRUE(delegate->is_intercepted());
delegate->LoadResponseBody("__MSG_hello__!");
delegate->CompleteResponse();
delegate->destination_loader_client()->RunUntilComplete();
std::string response;
EXPECT_TRUE(mojo::BlockingCopyToString(
delegate->destination_loader_client()->response_body_release(),
&response));
EXPECT_EQ("hola!", response);
EXPECT_EQ(
net::OK,
delegate->destination_loader_client()->completion_status().error_code);
}
TEST_F(ExtensionLocalizationThrottleTest, TwoMessages) {
const GURL url("chrome-extension://some_id/test.css");
auto throttle =
ExtensionLocalizationThrottle::MaybeCreate(blink::WebURL(url));
ASSERT_TRUE(throttle);
auto delegate = std::make_unique<FakeDelegate>();
throttle->set_delegate(delegate.get());
auto response_head = network::mojom::URLResponseHead::New();
response_head->mime_type = "text/css";
bool defer = false;
throttle->WillProcessResponse(url, response_head.get(), &defer);
EXPECT_FALSE(defer);
EXPECT_TRUE(delegate->is_intercepted());
delegate->LoadResponseBody("__MSG_hello__ __MSG");
task_environment_.RunUntilIdle();
delegate->LoadResponseBody("_world__!");
delegate->CompleteResponse();
delegate->destination_loader_client()->RunUntilComplete();
std::string response;
EXPECT_TRUE(mojo::BlockingCopyToString(
delegate->destination_loader_client()->response_body_release(),
&response));
EXPECT_EQ("hola mundo!", response);
EXPECT_EQ(
net::OK,
delegate->destination_loader_client()->completion_status().error_code);
}
TEST_F(ExtensionLocalizationThrottleTest, EmptyData) {
const GURL url("chrome-extension://some_id/test.css");
auto throttle =
ExtensionLocalizationThrottle::MaybeCreate(blink::WebURL(url));
ASSERT_TRUE(throttle);
auto delegate = std::make_unique<FakeDelegate>();
throttle->set_delegate(delegate.get());
auto response_head = network::mojom::URLResponseHead::New();
response_head->mime_type = "text/css";
bool defer = false;
throttle->WillProcessResponse(url, response_head.get(), &defer);
EXPECT_FALSE(defer);
EXPECT_TRUE(delegate->is_intercepted());
delegate->CompleteResponse();
delegate->destination_loader_client()->RunUntilComplete();
std::string response;
EXPECT_TRUE(mojo::BlockingCopyToString(
delegate->destination_loader_client()->response_body_release(),
&response));
EXPECT_EQ("", response);
EXPECT_EQ(
net::OK,
delegate->destination_loader_client()->completion_status().error_code);
}
// Regression test for https://crbug.com/1475798
TEST_F(ExtensionLocalizationThrottleTest, Cancel) {
const GURL url("chrome-extension://some_id/test.css");
auto throttle =
ExtensionLocalizationThrottle::MaybeCreate(blink::WebURL(url));
ASSERT_TRUE(throttle);
auto delegate = std::make_unique<FakeDelegate>();
throttle->set_delegate(delegate.get());
auto response_head = network::mojom::URLResponseHead::New();
response_head->mime_type = "text/css";
bool defer = false;
throttle->WillProcessResponse(url, response_head.get(), &defer);
EXPECT_FALSE(defer);
EXPECT_TRUE(delegate->is_intercepted());
delegate->LoadResponseBody("__MSG_hello__!");
delegate->CompleteResponse();
// Run all tasks in the main thread to make DataPipeProducer::SequenceState
// call PostTask(&SequenceState::StartOnSequence) to a background thread.
base::RunLoop().RunUntilIdle();
// Resetting `destination_loader_remote` triggers
// ExtensionLocalizationURLLoader destruction.
delegate->destination_loader_remote().reset();
// Run all tasks in the main thread to destroy the
// ExtensionLocalizationURLLoader.
base::RunLoop().RunUntilIdle();
// Runs SequenceState::StartOnSequence in the background thread.
task_environment_.RunUntilIdle();
}
TEST_F(ExtensionLocalizationThrottleTest, SourceSideError) {
const GURL url("chrome-extension://some_id/test.css");
auto throttle =
ExtensionLocalizationThrottle::MaybeCreate(blink::WebURL(url));
ASSERT_TRUE(throttle);
auto delegate = std::make_unique<FakeDelegate>();
throttle->set_delegate(delegate.get());
auto response_head = network::mojom::URLResponseHead::New();
response_head->mime_type = "text/css";
bool defer = false;
throttle->WillProcessResponse(url, response_head.get(), &defer);
EXPECT_FALSE(defer);
EXPECT_TRUE(delegate->is_intercepted());
delegate->LoadResponseBody("__MSG_hello__!");
delegate->source_loader_client_remote()->OnComplete(
network::URLLoaderCompletionStatus(net::ERR_OUT_OF_MEMORY));
delegate->source_body_handle().reset();
delegate->destination_loader_client()->RunUntilComplete();
std::string response;
EXPECT_TRUE(mojo::BlockingCopyToString(
delegate->destination_loader_client()->response_body_release(),
&response));
EXPECT_EQ("hola!", response);
EXPECT_EQ(
net::ERR_OUT_OF_MEMORY,
delegate->destination_loader_client()->completion_status().error_code);
}
TEST_F(ExtensionLocalizationThrottleTest, WriteError) {
const GURL url("chrome-extension://some_id/test.css");
auto throttle =
ExtensionLocalizationThrottle::MaybeCreate(blink::WebURL(url));
ASSERT_TRUE(throttle);
auto delegate = std::make_unique<FakeDelegate>();
throttle->set_delegate(delegate.get());
auto response_head = network::mojom::URLResponseHead::New();
response_head->mime_type = "text/css";
bool defer = false;
throttle->WillProcessResponse(url, response_head.get(), &defer);
EXPECT_FALSE(defer);
EXPECT_TRUE(delegate->is_intercepted());
// Release the body to cause write error.
delegate->destination_loader_client()->response_body_release();
task_environment_.RunUntilIdle();
delegate->LoadResponseBody("__MSG_hello__!");
delegate->CompleteResponse();
delegate->destination_loader_client()->RunUntilComplete();
EXPECT_EQ(
net::ERR_INSUFFICIENT_RESOURCES,
delegate->destination_loader_client()->completion_status().error_code);
}
TEST_F(ExtensionLocalizationThrottleTest, CreateDataPipeError) {
const GURL url("chrome-extension://some_id/test.css");
auto throttle =
ExtensionLocalizationThrottle::MaybeCreate(blink::WebURL(url));
ASSERT_TRUE(throttle);
throttle->ForceCreateDataPipeErrorForTest();
auto delegate = std::make_unique<FakeDelegate>();
throttle->set_delegate(delegate.get());
auto response_head = network::mojom::URLResponseHead::New();
response_head->mime_type = "text/css";
bool defer = false;
throttle->WillProcessResponse(url, response_head.get(), &defer);
EXPECT_TRUE(defer);
EXPECT_FALSE(delegate->is_intercepted());
EXPECT_FALSE(delegate->cancel_error_code());
// Run loop to call DeferredCancelWithError().
base::RunLoop().RunUntilIdle();
ASSERT_TRUE(delegate->cancel_error_code());
EXPECT_EQ(net::ERR_INSUFFICIENT_RESOURCES, *delegate->cancel_error_code());
ASSERT_TRUE(delegate->cancel_custom_reason());
EXPECT_EQ("ExtensionLocalizationThrottle", *delegate->cancel_custom_reason());
}
TEST_F(ExtensionLocalizationThrottleTest, URLLoaderChain) {
const GURL url("chrome-extension://some_id/test.css");
auto throttle =
ExtensionLocalizationThrottle::MaybeCreate(blink::WebURL(url));
ASSERT_TRUE(throttle);
auto delegate = std::make_unique<FakeDelegate>();
throttle->set_delegate(delegate.get());
auto response_head = network::mojom::URLResponseHead::New();
response_head->mime_type = "text/css";
bool defer = false;
throttle->WillProcessResponse(url, response_head.get(), &defer);
EXPECT_FALSE(defer);
EXPECT_TRUE(delegate->is_intercepted());
FakeURLLoader* source_url_loader = delegate->source_url_loader();
mojo::Remote<network::mojom::URLLoader>& destination_loader_remote =
delegate->destination_loader_remote();
ASSERT_TRUE(source_url_loader);
EXPECT_FALSE(source_url_loader->set_priority_called());
EXPECT_EQ(FakeURLLoader::Status::kInitial, source_url_loader->status());
destination_loader_remote->SetPriority(net::LOW, 1);
task_environment_.RunUntilIdle();
EXPECT_TRUE(source_url_loader->set_priority_called());
destination_loader_remote->PauseReadingBodyFromNet();
task_environment_.RunUntilIdle();
EXPECT_EQ(FakeURLLoader::Status::kPauseReading, source_url_loader->status());
destination_loader_remote->ResumeReadingBodyFromNet();
task_environment_.RunUntilIdle();
EXPECT_EQ(FakeURLLoader::Status::kResumeReading, source_url_loader->status());
delegate->LoadResponseBody("__MSG_hello__!");
delegate->CompleteResponse();
delegate->destination_loader_client()->RunUntilComplete();
std::string response;
EXPECT_TRUE(mojo::BlockingCopyToString(
delegate->destination_loader_client()->response_body_release(),
&response));
EXPECT_EQ("hola!", response);
EXPECT_EQ(
net::OK,
delegate->destination_loader_client()->completion_status().error_code);
}
TEST_F(ExtensionLocalizationThrottleTest,
URLLoaderClientOnTransferSizeUpdated) {
const GURL url("chrome-extension://some_id/test.css");
auto throttle =
ExtensionLocalizationThrottle::MaybeCreate(blink::WebURL(url));
ASSERT_TRUE(throttle);
auto delegate = std::make_unique<FakeDelegate>();
throttle->set_delegate(delegate.get());
auto response_head = network::mojom::URLResponseHead::New();
response_head->mime_type = "text/css";
bool defer = false;
throttle->WillProcessResponse(url, response_head.get(), &defer);
EXPECT_FALSE(defer);
EXPECT_TRUE(delegate->is_intercepted());
network::TestURLLoaderClient* destination_loader_client =
delegate->destination_loader_client();
mojo::Remote<network::mojom::URLLoaderClient>& source_loader_client_remote =
delegate->source_loader_client_remote();
ASSERT_TRUE(destination_loader_client);
EXPECT_EQ(0, destination_loader_client->body_transfer_size());
source_loader_client_remote->OnTransferSizeUpdated(/*transfer_size_diff=*/10);
task_environment_.RunUntilIdle();
EXPECT_EQ(10, destination_loader_client->body_transfer_size());
delegate->LoadResponseBody("__MSG_hello__!");
delegate->CompleteResponse();
destination_loader_client->RunUntilComplete();
std::string response;
EXPECT_TRUE(mojo::BlockingCopyToString(
destination_loader_client->response_body_release(), &response));
EXPECT_EQ("hola!", response);
EXPECT_EQ(net::OK, destination_loader_client->completion_status().error_code);
}
} // namespace
} // namespace extensions