blob: 3c44fc1ce9592149a548a66058d34feade3e03c5 [file] [log] [blame]
// Copyright 2016 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/bindings/api_request_handler.h"
#include <optional>
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/strings/strcat.h"
#include "base/test/bind.h"
#include "base/values.h"
#include "extensions/renderer/bindings/api_binding_test.h"
#include "extensions/renderer/bindings/api_binding_test_util.h"
#include "extensions/renderer/bindings/exception_handler.h"
#include "extensions/renderer/bindings/test_interaction_provider.h"
#include "extensions/renderer/bindings/test_js_runner.h"
#include "extensions/renderer/v8_helpers.h"
#include "gin/converter.h"
#include "gin/function_template.h"
#include "gin/public/context_holder.h"
#include "gin/public/isolate_holder.h"
#include "testing/gmock/include/gmock/gmock.h"
namespace extensions {
namespace {
const char kEchoArgs[] =
"(function() { this.result = Array.from(arguments); })";
const char kMethod[] = "method";
// TODO(devlin): We should probably hoist this up to e.g. api_binding_types.h.
using ArgumentList = v8::LocalVector<v8::Value>;
// TODO(devlin): Should we move some parts of api_binding_unittest.cc to here?
} // namespace
class APIRequestHandlerTest : public APIBindingTest {
public:
APIRequestHandlerTest(const APIRequestHandlerTest&) = delete;
APIRequestHandlerTest& operator=(const APIRequestHandlerTest&) = delete;
std::unique_ptr<APIRequestHandler> CreateRequestHandler() {
return std::make_unique<APIRequestHandler>(
base::DoNothing(),
APILastError(APILastError::GetParent(), binding::AddConsoleError()),
exception_handler(), interaction_provider());
}
void SaveUserActivationState(v8::Local<v8::Context> context,
std::optional<bool>* ran_with_user_gesture) {
*ran_with_user_gesture =
interaction_provider()->HasActiveInteraction(context);
}
protected:
APIRequestHandlerTest() {}
~APIRequestHandlerTest() override {}
std::unique_ptr<TestJSRunner::Scope> CreateTestJSRunner() override {
return std::make_unique<TestJSRunner::Scope>(
std::make_unique<TestJSRunner>(base::BindRepeating(
&APIRequestHandlerTest::SetDidRunJS, base::Unretained(this))));
}
InteractionProvider* interaction_provider() {
if (!interaction_provider_)
interaction_provider_ = std::make_unique<TestInteractionProvider>();
return interaction_provider_.get();
}
ExceptionHandler* exception_handler() {
if (!exception_handler_) {
exception_handler_ =
std::make_unique<ExceptionHandler>(binding::AddConsoleError());
}
return exception_handler_.get();
}
bool did_run_js() const { return did_run_js_; }
private:
void SetDidRunJS() { did_run_js_ = true; }
bool did_run_js_ = false;
std::unique_ptr<TestInteractionProvider> interaction_provider_;
std::unique_ptr<ExceptionHandler> exception_handler_;
};
// Tests adding a request to the request handler, and then triggering the
// response.
TEST_F(APIRequestHandlerTest, AddRequestAndCompleteRequestTest) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
std::unique_ptr<APIRequestHandler> request_handler = CreateRequestHandler();
EXPECT_TRUE(request_handler->GetPendingRequestIdsForTesting().empty());
v8::Local<v8::Function> function = FunctionFromString(context, kEchoArgs);
ASSERT_FALSE(function.IsEmpty());
request_handler->StartRequest(context, kMethod, base::Value::List(),
binding::AsyncResponseType::kCallback, function,
v8::Local<v8::Function>(),
binding::ResultModifierFunction());
int request_id = request_handler->last_sent_request_id();
EXPECT_THAT(request_handler->GetPendingRequestIdsForTesting(),
testing::UnorderedElementsAre(request_id));
const char kArguments[] = "['foo',1,{'prop1':'bar'}]";
request_handler->CompleteRequest(request_id, ListValueFromString(kArguments),
std::string());
EXPECT_TRUE(did_run_js());
EXPECT_EQ(ReplaceSingleQuotes(kArguments),
GetStringPropertyFromObject(context->Global(), context, "result"));
EXPECT_TRUE(request_handler->GetPendingRequestIdsForTesting().empty());
request_handler->StartRequest(
context, kMethod, base::Value::List(), binding::AsyncResponseType::kNone,
v8::Local<v8::Function>(), v8::Local<v8::Function>(),
binding::ResultModifierFunction());
request_id = request_handler->last_sent_request_id();
EXPECT_NE(-1, request_id);
request_handler->CompleteRequest(request_id, base::Value::List(),
std::string());
}
// Tests that trying to run non-existent or invalided requests is a no-op.
TEST_F(APIRequestHandlerTest, InvalidRequestsTest) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
std::unique_ptr<APIRequestHandler> request_handler = CreateRequestHandler();
v8::Local<v8::Function> function = FunctionFromString(context, kEchoArgs);
ASSERT_FALSE(function.IsEmpty());
request_handler->StartRequest(context, kMethod, base::Value::List(),
binding::AsyncResponseType::kCallback, function,
v8::Local<v8::Function>(),
binding::ResultModifierFunction());
int request_id = request_handler->last_sent_request_id();
EXPECT_THAT(request_handler->GetPendingRequestIdsForTesting(),
testing::UnorderedElementsAre(request_id));
// Try running with a non-existent request id.
int fake_request_id = 42;
request_handler->CompleteRequest(
fake_request_id, ListValueFromString("['foo']"), std::string());
EXPECT_FALSE(did_run_js());
// Try running with a request from an invalidated context.
request_handler->InvalidateContext(context);
request_handler->CompleteRequest(request_id, ListValueFromString("['foo']"),
std::string());
EXPECT_FALSE(did_run_js());
}
TEST_F(APIRequestHandlerTest, MultipleRequestsAndContexts) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context_a = MainContext();
v8::Local<v8::Context> context_b = AddContext();
std::unique_ptr<APIRequestHandler> request_handler = CreateRequestHandler();
// By having both different arguments and different behaviors in the
// callbacks, we can easily verify that the right function is called in the
// right context.
v8::Local<v8::Function> function_a = FunctionFromString(
context_a, "(function(res) { this.result = res + 'alpha'; })");
v8::Local<v8::Function> function_b = FunctionFromString(
context_b, "(function(res) { this.result = res + 'beta'; })");
request_handler->StartRequest(context_a, kMethod, base::Value::List(),
binding::AsyncResponseType::kCallback,
function_a, v8::Local<v8::Function>(),
binding::ResultModifierFunction());
int request_a = request_handler->last_sent_request_id();
request_handler->StartRequest(context_b, kMethod, base::Value::List(),
binding::AsyncResponseType::kCallback,
function_b, v8::Local<v8::Function>(),
binding::ResultModifierFunction());
int request_b = request_handler->last_sent_request_id();
EXPECT_THAT(request_handler->GetPendingRequestIdsForTesting(),
testing::UnorderedElementsAre(request_a, request_b));
request_handler->CompleteRequest(
request_a, ListValueFromString("['response_a:']"), std::string());
EXPECT_TRUE(did_run_js());
EXPECT_THAT(request_handler->GetPendingRequestIdsForTesting(),
testing::UnorderedElementsAre(request_b));
EXPECT_EQ(
ReplaceSingleQuotes("'response_a:alpha'"),
GetStringPropertyFromObject(context_a->Global(), context_a, "result"));
request_handler->CompleteRequest(
request_b, ListValueFromString("['response_b:']"), std::string());
EXPECT_TRUE(request_handler->GetPendingRequestIdsForTesting().empty());
EXPECT_EQ(
ReplaceSingleQuotes("'response_b:beta'"),
GetStringPropertyFromObject(context_b->Global(), context_b, "result"));
}
TEST_F(APIRequestHandlerTest, CustomCallbackArguments) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
std::unique_ptr<APIRequestHandler> request_handler = CreateRequestHandler();
v8::Local<v8::Function> custom_callback =
FunctionFromString(context, kEchoArgs);
v8::Local<v8::Function> callback = FunctionFromString(
context, "(function(arg) {this.callbackCalled = arg})");
ASSERT_FALSE(callback.IsEmpty());
ASSERT_FALSE(custom_callback.IsEmpty());
request_handler->StartRequest(context, "method", base::Value::List(),
binding::AsyncResponseType::kCallback, callback,
custom_callback,
binding::ResultModifierFunction());
int request_id = request_handler->last_sent_request_id();
EXPECT_THAT(request_handler->GetPendingRequestIdsForTesting(),
testing::UnorderedElementsAre(request_id));
request_handler->CompleteRequest(
request_id, ListValueFromString("['response', 'arguments']"),
std::string());
EXPECT_TRUE(did_run_js());
v8::Local<v8::Array> result;
ASSERT_TRUE(
GetPropertyFromObjectAs(context->Global(), context, "result", &result));
ArgumentList args(isolate());
ASSERT_TRUE(gin::Converter<ArgumentList>::FromV8(isolate(), result, &args));
ASSERT_EQ(3u, args.size());
EXPECT_TRUE(args[0]->IsFunction());
EXPECT_EQ(R"("response")", V8ToString(args[1], context));
EXPECT_EQ(R"("arguments")", V8ToString(args[2], context));
EXPECT_TRUE(request_handler->GetPendingRequestIdsForTesting().empty());
// The function passed to the custom callback isn't actually the same callback
// that was passed in when calling the API, but invoking it below should still
// result in the original callback being run.
EXPECT_TRUE(
GetPropertyFromObject(context->Global(), context, "callbackCalled")
->IsUndefined());
v8::Local<v8::Value> callback_args[] = {gin::StringToV8(isolate(), "foo")};
RunFunctionOnGlobal(args[0].As<v8::Function>(), context, 1, callback_args);
EXPECT_EQ(R"("foo")", GetStringPropertyFromObject(context->Global(), context,
"callbackCalled"));
}
TEST_F(APIRequestHandlerTest, CustomCallbackWithErrorInExtensionCallback) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
auto add_console_error = [](std::optional<std::string>* error_out,
v8::Local<v8::Context> context,
const std::string& error) { *error_out = error; };
std::optional<std::string> logged_error;
ExceptionHandler exception_handler(
base::BindRepeating(add_console_error, &logged_error));
APIRequestHandler request_handler(
base::DoNothing(),
APILastError(APILastError::GetParent(), binding::AddConsoleError()),
&exception_handler, interaction_provider());
constexpr char kExtensionCallback[] =
R"((function() {
this.callbackCalled = true;
throw new Error('hello');
}))";
v8::Local<v8::Function> callback_throwing_error =
FunctionFromString(context, kExtensionCallback);
constexpr char kCustomCallback[] =
R"((function(callback) {
this.customCallbackCalled = true;
callback();
}))";
v8::Local<v8::Function> custom_callback =
FunctionFromString(context, kCustomCallback);
ASSERT_FALSE(callback_throwing_error.IsEmpty());
ASSERT_FALSE(custom_callback.IsEmpty());
request_handler.StartRequest(context, "method", base::Value::List(),
binding::AsyncResponseType::kCallback,
callback_throwing_error, custom_callback,
binding::ResultModifierFunction());
int request_id = request_handler.last_sent_request_id();
EXPECT_THAT(request_handler.GetPendingRequestIdsForTesting(),
testing::UnorderedElementsAre(request_id));
v8::TryCatch try_catch(isolate());
{
TestJSRunner::AllowErrors allow_errors;
request_handler.CompleteRequest(request_id, base::Value::List(),
std::string());
}
EXPECT_TRUE(did_run_js());
EXPECT_TRUE(request_handler.GetPendingRequestIdsForTesting().empty());
EXPECT_EQ("true", GetStringPropertyFromObject(context->Global(), context,
"customCallbackCalled"));
EXPECT_EQ("true", GetStringPropertyFromObject(context->Global(), context,
"callbackCalled"));
// The `try_catch` should not have caught an error. This is important to not
// disrupt our bindings code (or other running JS) when asynchronously
// returning from an API call. Instead, the error should be caught and handled
// by the exception handler.
EXPECT_FALSE(try_catch.HasCaught());
ASSERT_TRUE(logged_error);
EXPECT_THAT(*logged_error,
testing::StartsWith("Error handling response: Error: hello"));
}
TEST_F(APIRequestHandlerTest, CustomCallbackPromiseBased) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
std::unique_ptr<APIRequestHandler> request_handler = CreateRequestHandler();
v8::Local<v8::Function> custom_callback =
FunctionFromString(context, kEchoArgs);
ASSERT_FALSE(custom_callback.IsEmpty());
v8::Local<v8::Promise> promise = request_handler->StartRequest(
context, "method", base::Value::List(),
binding::AsyncResponseType::kPromise, v8::Local<v8::Function>(),
custom_callback, binding::ResultModifierFunction());
ASSERT_FALSE(promise.IsEmpty());
int request_id = request_handler->last_sent_request_id();
EXPECT_THAT(request_handler->GetPendingRequestIdsForTesting(),
testing::UnorderedElementsAre(request_id));
request_handler->CompleteRequest(
request_id, ListValueFromString("['response', 'arguments']"),
std::string());
EXPECT_TRUE(did_run_js());
v8::Local<v8::Array> result;
ASSERT_TRUE(
GetPropertyFromObjectAs(context->Global(), context, "result", &result));
ArgumentList args(isolate());
ASSERT_TRUE(gin::Converter<ArgumentList>::FromV8(isolate(), result, &args));
ASSERT_EQ(3u, args.size());
// Even though this is a promise based request the custom callbacks expect a
// function argument to be passed to them, hence why we get a function here.
// Invoking the callback however, should still result in the promise being
// resolved.
EXPECT_TRUE(args[0]->IsFunction());
EXPECT_EQ(R"("response")", V8ToString(args[1], context));
EXPECT_EQ(R"("arguments")", V8ToString(args[2], context));
EXPECT_TRUE(request_handler->GetPendingRequestIdsForTesting().empty());
EXPECT_EQ(v8::Promise::kPending, promise->State());
v8::Local<v8::Value> callback_args[] = {gin::StringToV8(isolate(), "foo")};
RunFunctionOnGlobal(args[0].As<v8::Function>(), context, 1, callback_args);
EXPECT_EQ(v8::Promise::kFulfilled, promise->State());
EXPECT_EQ(R"("foo")", V8ToString(promise->Result(), context));
}
// Test that having a custom callback without an extension-provided callback
// doesn't crash.
TEST_F(APIRequestHandlerTest, CustomCallbackArgumentsWithEmptyCallback) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
std::unique_ptr<APIRequestHandler> request_handler = CreateRequestHandler();
v8::Local<v8::Function> custom_callback =
FunctionFromString(context, kEchoArgs);
ASSERT_FALSE(custom_callback.IsEmpty());
v8::Local<v8::Function> empty_callback;
request_handler->StartRequest(
context, "method", base::Value::List(), binding::AsyncResponseType::kNone,
empty_callback, custom_callback, binding::ResultModifierFunction());
int request_id = request_handler->last_sent_request_id();
EXPECT_THAT(request_handler->GetPendingRequestIdsForTesting(),
testing::UnorderedElementsAre(request_id));
request_handler->CompleteRequest(request_id, base::Value::List(),
std::string());
EXPECT_TRUE(did_run_js());
v8::Local<v8::Array> result;
ASSERT_TRUE(
GetPropertyFromObjectAs(context->Global(), context, "result", &result));
ArgumentList args(isolate());
ASSERT_TRUE(gin::Converter<ArgumentList>::FromV8(isolate(), result, &args));
ASSERT_EQ(1u, args.size());
EXPECT_TRUE(args[0]->IsUndefined());
EXPECT_TRUE(request_handler->GetPendingRequestIdsForTesting().empty());
}
TEST_F(APIRequestHandlerTest, ResultModifier) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
binding::ResultModifierFunction result_modifier =
base::BindOnce([](const v8::LocalVector<v8::Value>& result_args,
v8::Local<v8::Context> context,
binding::AsyncResponseType async_type) {
EXPECT_EQ(1u, result_args.size());
EXPECT_TRUE(result_args[0]->IsObject());
v8::Local<v8::Object> result_obj = result_args[0].As<v8::Object>();
v8::Local<v8::Value> prop_1;
bool success =
v8_helpers::GetProperty(context, result_obj, "prop1", &prop_1);
DCHECK(success);
v8::Local<v8::Value> prop_2;
success =
v8_helpers::GetProperty(context, result_obj, "prop2", &prop_2);
DCHECK(success);
v8::LocalVector<v8::Value> new_args(v8::Isolate::GetCurrent(),
{prop_1, prop_2});
return new_args;
});
std::unique_ptr<APIRequestHandler> request_handler = CreateRequestHandler();
v8::Local<v8::Function> callback = FunctionFromString(
context, "(function(arg1, arg2) {this.arg1 = arg1; this.arg2 = arg2});");
ASSERT_FALSE(callback.IsEmpty());
request_handler->StartRequest(context, "method", base::Value::List(),
binding::AsyncResponseType::kCallback, callback,
v8::Local<v8::Function>(),
std::move(result_modifier));
int request_id = request_handler->last_sent_request_id();
EXPECT_THAT(request_handler->GetPendingRequestIdsForTesting(),
testing::UnorderedElementsAre(request_id));
request_handler->CompleteRequest(
request_id, ListValueFromString("[{'prop1':'foo', 'prop2':'bar'}]"),
std::string());
EXPECT_TRUE(did_run_js());
EXPECT_TRUE(request_handler->GetPendingRequestIdsForTesting().empty());
EXPECT_EQ(R"("foo")",
GetStringPropertyFromObject(context->Global(), context, "arg1"));
EXPECT_EQ(R"("bar")",
GetStringPropertyFromObject(context->Global(), context, "arg2"));
}
// Test user gestures being curried around for API requests.
TEST_F(APIRequestHandlerTest, UserGestureTest) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
std::unique_ptr<APIRequestHandler> request_handler = CreateRequestHandler();
// Set up a callback to be used with the request so we can check if a user
// gesture was active.
std::optional<bool> ran_with_user_gesture;
v8::Local<v8::FunctionTemplate> function_template =
gin::CreateFunctionTemplate(
isolate(),
base::BindRepeating(&APIRequestHandlerTest::SaveUserActivationState,
base::Unretained(this), context,
&ran_with_user_gesture));
v8::Local<v8::Function> v8_callback =
function_template->GetFunction(context).ToLocalChecked();
// Try first without a user gesture.
request_handler->StartRequest(context, kMethod, base::Value::List(),
binding::AsyncResponseType::kCallback,
v8_callback, v8::Local<v8::Function>(),
binding::ResultModifierFunction());
int request_id = request_handler->last_sent_request_id();
request_handler->CompleteRequest(request_id, ListValueFromString("[]"),
std::string());
ASSERT_TRUE(ran_with_user_gesture);
EXPECT_FALSE(*ran_with_user_gesture);
ran_with_user_gesture.reset();
// Next try calling with a user gesture. Since a gesture will be active at the
// time of the call, it should also be active during the callback.
ScopedTestUserActivation test_user_activation;
// TODO(devlin): This isn't quite right with UAv1/UAv2. V1 should properly
// activate a new user gesture on the stack, and v2 should rely on the gesture
// being persisted (or generated from the browser). We should clean this up.
EXPECT_TRUE(interaction_provider()->HasActiveInteraction(context));
request_handler->StartRequest(context, kMethod, base::Value::List(),
binding::AsyncResponseType::kCallback,
v8_callback, v8::Local<v8::Function>(),
binding::ResultModifierFunction());
request_id = request_handler->last_sent_request_id();
request_handler->CompleteRequest(request_id, ListValueFromString("[]"),
std::string());
ASSERT_TRUE(ran_with_user_gesture);
EXPECT_TRUE(*ran_with_user_gesture);
// Sanity check: the callback doesn't change the state
EXPECT_TRUE(interaction_provider()->HasActiveInteraction(context));
}
TEST_F(APIRequestHandlerTest, SettingLastError) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
std::optional<std::string> logged_error;
auto get_parent = [](v8::Local<v8::Context> context,
v8::Local<v8::Object>* secondary_parent) {
return context->Global();
};
auto log_error = [](std::optional<std::string>* logged_error,
v8::Local<v8::Context> context,
const std::string& error) { *logged_error = error; };
APIRequestHandler request_handler(
base::DoNothing(),
APILastError(base::BindRepeating(get_parent),
base::BindRepeating(log_error, &logged_error)),
exception_handler(), interaction_provider());
const char kReportExposedLastError[] =
"(function() {\n"
" if (this.lastError)\n"
" this.seenLastError = this.lastError.message;\n"
"})";
auto get_exposed_error = [context]() {
return GetStringPropertyFromObject(context->Global(), context,
"seenLastError");
};
{
// Test a successful function call. No last error should be emitted to the
// console or exposed to the callback.
v8::Local<v8::Function> callback =
FunctionFromString(context, kReportExposedLastError);
request_handler.StartRequest(context, kMethod, base::Value::List(),
binding::AsyncResponseType::kCallback,
callback, v8::Local<v8::Function>(),
binding::ResultModifierFunction());
int request_id = request_handler.last_sent_request_id();
request_handler.CompleteRequest(request_id, base::Value::List(),
std::string());
EXPECT_FALSE(logged_error);
EXPECT_EQ("undefined", get_exposed_error());
logged_error.reset();
}
{
// Test a function call resulting in an error. Since the callback checks the
// last error, no error should be logged to the console (but it should be
// exposed to the callback).
v8::Local<v8::Function> callback =
FunctionFromString(context, kReportExposedLastError);
request_handler.StartRequest(context, kMethod, base::Value::List(),
binding::AsyncResponseType::kCallback,
callback, v8::Local<v8::Function>(),
binding::ResultModifierFunction());
int request_id = request_handler.last_sent_request_id();
request_handler.CompleteRequest(request_id, base::Value::List(),
"some error");
EXPECT_FALSE(logged_error);
EXPECT_EQ("\"some error\"", get_exposed_error());
logged_error.reset();
}
{
// Test a function call resulting in an error that goes unchecked by the
// callback. The error should be logged.
v8::Local<v8::Function> callback =
FunctionFromString(context, "(function() {})");
request_handler.StartRequest(context, kMethod, base::Value::List(),
binding::AsyncResponseType::kCallback,
callback, v8::Local<v8::Function>(),
binding::ResultModifierFunction());
int request_id = request_handler.last_sent_request_id();
request_handler.CompleteRequest(request_id, base::Value::List(),
"some error");
ASSERT_TRUE(logged_error);
EXPECT_EQ("Unchecked runtime.lastError: some error", *logged_error);
logged_error.reset();
}
{
// Test a function call resulting in an error with only a custom callback,
// and no author-script-provided callback. The error should be logged.
v8::Local<v8::Function> custom_callback =
FunctionFromString(context, "(function() {})");
request_handler.StartRequest(context, kMethod, base::Value::List(),
binding::AsyncResponseType::kNone,
v8::Local<v8::Function>(), custom_callback,
binding::ResultModifierFunction());
int request_id = request_handler.last_sent_request_id();
request_handler.CompleteRequest(request_id, base::Value::List(),
"some error");
ASSERT_TRUE(logged_error);
EXPECT_EQ("Unchecked runtime.lastError: some error", *logged_error);
logged_error.reset();
}
{
// Test a function call resulting in an error that does not have an
// associated callback callback. The error should be logged.
request_handler.StartRequest(
context, kMethod, base::Value::List(),
binding::AsyncResponseType::kNone, v8::Local<v8::Function>(),
v8::Local<v8::Function>(), binding::ResultModifierFunction());
int request_id = request_handler.last_sent_request_id();
request_handler.CompleteRequest(request_id, base::Value::List(),
"some error");
ASSERT_TRUE(logged_error);
EXPECT_EQ("Unchecked runtime.lastError: some error", *logged_error);
logged_error.reset();
}
{
// Test a function call resulting in an error for a request handler that has
// an associated result modifier. The result modifier should never be called
// and since the callback checks last error no error should be logged to the
// console.
bool result_modifier_called = false;
auto result_modifier = [&result_modifier_called](
const v8::LocalVector<v8::Value>& result_args,
v8::Local<v8::Context> context,
binding::AsyncResponseType async_type) {
result_modifier_called = true;
return result_args;
};
v8::Local<v8::Function> callback =
FunctionFromString(context, kReportExposedLastError);
request_handler.StartRequest(context, kMethod, base::Value::List(),
binding::AsyncResponseType::kCallback,
callback, v8::Local<v8::Function>(),
base::BindLambdaForTesting(result_modifier));
int request_id = request_handler.last_sent_request_id();
request_handler.CompleteRequest(request_id, base::Value::List(),
"some error");
EXPECT_FALSE(logged_error);
EXPECT_EQ("\"some error\"", get_exposed_error());
EXPECT_FALSE(result_modifier_called);
logged_error.reset();
}
}
TEST_F(APIRequestHandlerTest, AddPendingRequestCallback) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
bool dispatched_request = false;
auto handle_request = [](bool* dispatched_request,
std::unique_ptr<APIRequestHandler::Request> request,
v8::Local<v8::Context> context) {
*dispatched_request = true;
};
APIRequestHandler request_handler(
base::BindRepeating(handle_request, &dispatched_request),
APILastError(APILastError::GetParent(), binding::AddConsoleError()),
exception_handler(), interaction_provider());
EXPECT_TRUE(request_handler.GetPendingRequestIdsForTesting().empty());
v8::Local<v8::Function> function = FunctionFromString(context, kEchoArgs);
ASSERT_FALSE(function.IsEmpty());
auto details = request_handler.AddPendingRequest(
context, binding::AsyncResponseType::kCallback, function,
binding::ResultModifierFunction());
int request_id = details.request_id;
EXPECT_TRUE(details.promise.IsEmpty());
EXPECT_THAT(request_handler.GetPendingRequestIdsForTesting(),
testing::UnorderedElementsAre(request_id));
// Even though we add a pending request, we shouldn't have dispatched anything
// because AddPendingRequest() is intended for renderer-side implementations.
EXPECT_FALSE(dispatched_request);
const char kArguments[] = "['foo',1,{'prop1':'bar'}]";
request_handler.CompleteRequest(request_id, ListValueFromString(kArguments),
std::string());
EXPECT_EQ(ReplaceSingleQuotes(kArguments),
GetStringPropertyFromObject(context->Global(), context, "result"));
EXPECT_TRUE(request_handler.GetPendingRequestIdsForTesting().empty());
EXPECT_FALSE(dispatched_request);
}
TEST_F(APIRequestHandlerTest, AddPendingRequestPromise) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
bool dispatched_request = false;
auto handle_request = [](bool* dispatched_request,
std::unique_ptr<APIRequestHandler::Request> request,
v8::Local<v8::Context> context) {
*dispatched_request = true;
};
APIRequestHandler request_handler(
base::BindRepeating(handle_request, &dispatched_request),
APILastError(APILastError::GetParent(), binding::AddConsoleError()),
exception_handler(), interaction_provider());
EXPECT_TRUE(request_handler.GetPendingRequestIdsForTesting().empty());
auto details = request_handler.AddPendingRequest(
context, binding::AsyncResponseType::kPromise, v8::Local<v8::Function>(),
binding::ResultModifierFunction());
int request_id = details.request_id;
v8::Local<v8::Promise> promise = details.promise;
EXPECT_THAT(request_handler.GetPendingRequestIdsForTesting(),
testing::UnorderedElementsAre(request_id));
ASSERT_FALSE(promise.IsEmpty());
EXPECT_EQ(v8::Promise::kPending, promise->State());
// Even though we add a pending request, we shouldn't have dispatched anything
// because AddPendingRequest() is intended for renderer-side implementations.
EXPECT_FALSE(dispatched_request);
request_handler.CompleteRequest(
request_id, ListValueFromString("[{'foo': 'bar'}]"), std::string());
ASSERT_EQ(v8::Promise::kFulfilled, promise->State());
EXPECT_EQ(R"({"foo":"bar"})", V8ToString(promise->Result(), context));
EXPECT_TRUE(request_handler.GetPendingRequestIdsForTesting().empty());
EXPECT_FALSE(dispatched_request);
}
TEST_F(APIRequestHandlerTest, AddPendingRequestWithResultModifier) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
binding::ResultModifierFunction result_modifier =
base::BindOnce([](const v8::LocalVector<v8::Value>& result_args,
v8::Local<v8::Context> context,
binding::AsyncResponseType async_type) {
DCHECK_EQ(1u, result_args.size());
DCHECK(result_args[0]->IsObject());
v8::Local<v8::Object> result_obj = result_args[0].As<v8::Object>();
v8::Local<v8::Value> prop_1;
bool success =
v8_helpers::GetProperty(context, result_obj, "prop1", &prop_1);
DCHECK(success);
v8::Local<v8::Value> prop_2;
success =
v8_helpers::GetProperty(context, result_obj, "prop2", &prop_2);
DCHECK(success);
v8::LocalVector<v8::Value> new_args(v8::Isolate::GetCurrent(),
{prop_1, prop_2});
return new_args;
});
std::unique_ptr<APIRequestHandler> request_handler = CreateRequestHandler();
v8::Local<v8::Function> function = FunctionFromString(context, kEchoArgs);
ASSERT_FALSE(function.IsEmpty());
auto details = request_handler->AddPendingRequest(
context, binding::AsyncResponseType::kCallback, function,
std::move(result_modifier));
int request_id = details.request_id;
EXPECT_TRUE(details.promise.IsEmpty());
const char kArguments[] = "[{'prop1':'bar', 'prop2':'baz'}]";
request_handler->CompleteRequest(request_id, ListValueFromString(kArguments),
std::string());
EXPECT_EQ(R"(["bar","baz"])",
GetStringPropertyFromObject(context->Global(), context, "result"));
}
// Tests that throwing an exception in a callback is properly handled.
TEST_F(APIRequestHandlerTest, ThrowExceptionInCallback) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
auto add_console_error = [](std::optional<std::string>* error_out,
v8::Local<v8::Context> context,
const std::string& error) { *error_out = error; };
std::optional<std::string> logged_error;
ExceptionHandler exception_handler(
base::BindRepeating(add_console_error, &logged_error));
APIRequestHandler request_handler(
base::DoNothing(),
APILastError(APILastError::GetParent(), binding::AddConsoleError()),
&exception_handler, interaction_provider());
v8::TryCatch outer_try_catch(isolate());
v8::Local<v8::Function> callback_throwing_error =
FunctionFromString(context, "(function() { throw new Error('hello'); })");
auto details = request_handler.AddPendingRequest(
context, binding::AsyncResponseType::kCallback, callback_throwing_error,
binding::ResultModifierFunction());
int request_id = details.request_id;
EXPECT_TRUE(details.promise.IsEmpty());
{
TestJSRunner::AllowErrors allow_errors;
request_handler.CompleteRequest(request_id, base::Value::List(),
std::string());
}
// |outer_try_catch| should not have caught an error. This is important to not
// disrupt our bindings code (or other running JS) when asynchronously
// returning from an API call. Instead, the error should be caught and handled
// by the exception handler.
EXPECT_FALSE(outer_try_catch.HasCaught());
ASSERT_TRUE(logged_error);
EXPECT_THAT(*logged_error,
testing::StartsWith("Error handling response: Error: hello"));
}
// Tests promise-based requests with the promise being fulfilled.
TEST_F(APIRequestHandlerTest, PromiseBasedRequests_Fulfilled) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
std::unique_ptr<APIRequestHandler> request_handler = CreateRequestHandler();
EXPECT_TRUE(request_handler->GetPendingRequestIdsForTesting().empty());
v8::Local<v8::Promise> promise = request_handler->StartRequest(
context, kMethod, base::Value::List(),
binding::AsyncResponseType::kPromise, v8::Local<v8::Function>(),
v8::Local<v8::Function>(), binding::ResultModifierFunction());
ASSERT_FALSE(promise.IsEmpty());
int request_id = request_handler->last_sent_request_id();
EXPECT_NE(-1, request_id);
EXPECT_THAT(request_handler->GetPendingRequestIdsForTesting(),
testing::UnorderedElementsAre(request_id));
EXPECT_EQ(v8::Promise::kPending, promise->State());
request_handler->CompleteRequest(request_id, ListValueFromString("['foo']"),
std::string());
ASSERT_EQ(v8::Promise::kFulfilled, promise->State());
EXPECT_EQ(R"("foo")", V8ToString(promise->Result(), context));
EXPECT_TRUE(request_handler->GetPendingRequestIdsForTesting().empty());
}
// Tests promise-based requests with the promise being rejected.
TEST_F(APIRequestHandlerTest, PromiseBasedRequests_Rejected) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
std::unique_ptr<APIRequestHandler> request_handler = CreateRequestHandler();
EXPECT_TRUE(request_handler->GetPendingRequestIdsForTesting().empty());
v8::Local<v8::Promise> promise = request_handler->StartRequest(
context, kMethod, base::Value::List(),
binding::AsyncResponseType::kPromise, v8::Local<v8::Function>(),
v8::Local<v8::Function>(), binding::ResultModifierFunction());
ASSERT_FALSE(promise.IsEmpty());
int request_id = request_handler->last_sent_request_id();
EXPECT_NE(-1, request_id);
EXPECT_THAT(request_handler->GetPendingRequestIdsForTesting(),
testing::UnorderedElementsAre(request_id));
EXPECT_EQ(v8::Promise::kPending, promise->State());
constexpr char kError[] = "Something went wrong!";
request_handler->CompleteRequest(request_id, base::Value::List(), kError);
ASSERT_EQ(v8::Promise::kRejected, promise->State());
v8::Local<v8::Value> result = promise->Result();
ASSERT_FALSE(result.IsEmpty());
EXPECT_EQ(
base::StrCat({"Error: ", kError}),
gin::V8ToString(isolate(), result->ToString(context).ToLocalChecked()));
EXPECT_TRUE(request_handler->GetPendingRequestIdsForTesting().empty());
}
} // namespace extensions