blob: 5111e3d8729de47555854d7a8c860860900e4f5f [file] [log] [blame] [edit]
// 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_binding.h"
#include <string_view>
#include <tuple>
#include "base/auto_reset.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/strings/stringprintf.h"
#include "base/test/bind.h"
#include "base/values.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "extensions/renderer/bindings/api_binding_hooks.h"
#include "extensions/renderer/bindings/api_binding_hooks_test_delegate.h"
#include "extensions/renderer/bindings/api_binding_test.h"
#include "extensions/renderer/bindings/api_binding_test_util.h"
#include "extensions/renderer/bindings/api_binding_types.h"
#include "extensions/renderer/bindings/api_binding_util.h"
#include "extensions/renderer/bindings/api_event_handler.h"
#include "extensions/renderer/bindings/api_invocation_errors.h"
#include "extensions/renderer/bindings/api_request_handler.h"
#include "extensions/renderer/bindings/api_signature.h"
#include "extensions/renderer/bindings/api_type_reference_map.h"
#include "extensions/renderer/bindings/binding_access_checker.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 "gin/arguments.h"
#include "gin/converter.h"
#include "gin/public/context_holder.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "v8/include/v8.h"
namespace extensions {
namespace {
const char kBindingName[] = "test";
// Function spec; we use single quotes for readability and then replace them.
const char kFunctions[] =
"[{"
" 'name': 'oneString',"
" 'parameters': [{"
" 'type': 'string',"
" 'name': 'str'"
" }]"
"}, {"
" 'name': 'stringAndInt',"
" 'parameters': [{"
" 'type': 'string',"
" 'name': 'str'"
" }, {"
" 'type': 'integer',"
" 'name': 'int'"
" }]"
"}, {"
" 'name': 'oneObject',"
" 'parameters': [{"
" 'type': 'object',"
" 'name': 'foo',"
" 'properties': {"
" 'prop1': {'type': 'string'},"
" 'prop2': {'type': 'string', 'optional': true}"
" }"
" }]"
"}, {"
" 'name': 'intAndCallback',"
" 'parameters': [{"
" 'name': 'int',"
" 'type': 'integer'"
" }],"
" 'returns_async': {"
" 'name': 'callback',"
" 'type': 'function'"
" }"
"}]";
constexpr char kFunctionsWithCallbackSignatures[] = R"(
[{
"name": "noCallback",
"parameters": [{
"name": "int",
"type": "integer"
}]
}, {
"name": "intCallback",
"parameters": [],
"returns_async": {
"name": "callback",
"does_not_support_promises": "Test",
"parameters": [{
"name": "int",
"type": "integer"
}]
}
}, {
"name": "noParamCallback",
"parameters": [],
"returns_async": {
"name": "callback",
"does_not_support_promises": "Test",
"parameters": []
}
}])";
constexpr char kFunctionsWithPromiseSignatures[] =
R"([{
"name": "supportsPromises",
"parameters": [{
"name": "int",
"type": "integer"
}],
"returns_async": {
"name": "callback",
"parameters": [{
"name": "strResult",
"type": "string"
}]
}
},
{
"name": "callbackOptional",
"parameters": [{
"name": "int",
"type": "integer"
}],
"returns_async": {
"name": "callback",
"optional": true,
"parameters": [{
"name": "strResult",
"type": "string"
}]
}
}])";
bool AllowAllFeatures(v8::Local<v8::Context> context, const std::string& name) {
return true;
}
bool DisallowPromises(v8::Local<v8::Context> context) {
return false;
}
void OnEventListenersChanged(const std::string& event_name,
binding::EventListenersChanged change,
const base::Value::Dict* filter,
bool was_manual,
v8::Local<v8::Context> context) {}
} // namespace
class APIBindingUnittest : public APIBindingTest {
public:
APIBindingUnittest(const APIBindingUnittest&) = delete;
APIBindingUnittest& operator=(const APIBindingUnittest&) = delete;
void OnFunctionCall(std::unique_ptr<APIRequestHandler::Request> request,
v8::Local<v8::Context> context) {
last_request_ = std::move(request);
}
using GetParentCallback = base::RepeatingCallback<v8::Local<v8::Object>()>;
v8::Local<v8::Object> GetParent(v8::Local<v8::Context> context,
v8::Local<v8::Object>* secondary_parent) {
DCHECK(!get_last_error_parent_.is_null())
<< "You must have get_last_error_parent_ set if a test is dealing with"
"lastError being set";
return get_last_error_parent_.Run();
}
void AddConsoleError(v8::Local<v8::Context> context,
const std::string& error) {
console_errors_.push_back(error);
}
protected:
APIBindingUnittest()
: type_refs_(APITypeReferenceMap::InitializeTypeCallback()) {}
void SetUp() override {
APIBindingTest::SetUp();
interaction_provider_ = std::make_unique<TestInteractionProvider>();
binding::AddConsoleError add_console_error(base::BindRepeating(
&APIBindingUnittest::AddConsoleError, base::Unretained(this)));
exception_handler_ = std::make_unique<ExceptionHandler>(add_console_error);
request_handler_ = std::make_unique<APIRequestHandler>(
base::BindRepeating(&APIBindingUnittest::OnFunctionCall,
base::Unretained(this)),
APILastError(base::BindRepeating(&APIBindingUnittest::GetParent,
base::Unretained(this)),
add_console_error),
exception_handler_.get(), interaction_provider_.get());
}
void TearDown() override {
DisposeAllContexts();
access_checker_.reset();
interaction_provider_.reset();
request_handler_.reset();
event_handler_.reset();
binding_.reset();
APIBindingTest::TearDown();
}
void OnWillDisposeContext(v8::Local<v8::Context> context) override {
event_handler_->InvalidateContext(context);
request_handler_->InvalidateContext(context);
}
void SetFunctions(const char* functions) {
binding_functions_ = ListValueFromString(functions);
}
void SetEvents(const char* events) {
binding_events_ = ListValueFromString(events);
}
void SetTypes(const char* types) {
binding_types_ = ListValueFromString(types);
}
void SetProperties(const char* properties) {
binding_properties_ = DictValueFromString(properties);
}
void SetHooks(std::unique_ptr<APIBindingHooks> hooks) {
binding_hooks_ = std::move(hooks);
ASSERT_TRUE(binding_hooks_);
}
void SetHooksDelegate(
std::unique_ptr<APIBindingHooksDelegate> hooks_delegate) {
binding_hooks_delegate_ = std::move(hooks_delegate);
ASSERT_TRUE(binding_hooks_delegate_);
}
void SetCreateCustomType(const APIBinding::CreateCustomType& callback) {
create_custom_type_ = callback;
}
void SetOnSilentRequest(const APIBinding::OnSilentRequest& callback) {
on_silent_request_ = callback;
}
void SetAPIAvailabilityCallback(
const BindingAccessChecker::APIAvailabilityCallback& callback) {
api_availability_callback_ = callback;
}
void SetPromiseAvailabilityFlag(bool* availability_flag) {
promise_availability_callback_ = base::BindRepeating(
[](bool* flag, v8::Local<v8::Context> context) { return *flag; },
availability_flag);
}
void SetLastErrorParentCallback(GetParentCallback get_parent) {
get_last_error_parent_ = std::move(get_parent);
}
void ClearConsoleErrors() { console_errors_.clear(); }
void InitializeJSHooks(
const char* register_hook,
v8::Local<v8::Value> additional_arg = v8::Local<v8::Value>()) {
auto hooks =
std::make_unique<APIBindingHooks>(kBindingName, request_handler());
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
{
v8::Local<v8::Object> js_hooks = hooks->GetJSHookInterface(context);
v8::Local<v8::Function> function =
FunctionFromString(context, register_hook);
if (!additional_arg.IsEmpty()) {
v8::Local<v8::Value> args[] = {js_hooks, additional_arg};
RunFunctionOnGlobal(function, context, std::size(args), args);
} else {
v8::Local<v8::Value> args[] = {js_hooks};
RunFunctionOnGlobal(function, context, std::size(args), args);
}
}
SetHooks(std::move(hooks));
}
void InitializeBinding() {
if (!binding_hooks_)
binding_hooks_ =
std::make_unique<APIBindingHooks>(kBindingName, request_handler());
if (binding_hooks_delegate_)
binding_hooks_->SetDelegate(std::move(binding_hooks_delegate_));
if (!on_silent_request_)
on_silent_request_ = base::DoNothing();
if (!api_availability_callback_)
api_availability_callback_ = base::BindRepeating(&AllowAllFeatures);
if (!promise_availability_callback_)
promise_availability_callback_ = base::BindRepeating(&DisallowPromises);
auto get_context_owner = [](v8::Local<v8::Context>) {
return std::string("context");
};
event_handler_ = std::make_unique<APIEventHandler>(
base::BindRepeating(&OnEventListenersChanged),
base::BindRepeating(get_context_owner), nullptr);
access_checker_ = std::make_unique<BindingAccessChecker>(
api_availability_callback_, promise_availability_callback_);
binding_ = std::make_unique<APIBinding>(
kBindingName, &binding_functions_, &binding_types_, &binding_events_,
&binding_properties_, create_custom_type_, on_silent_request_,
std::move(binding_hooks_), &type_refs_, request_handler_.get(),
event_handler_.get(), access_checker_.get());
}
v8::Local<v8::Value> ExpectPass(
v8::Local<v8::Object> object,
const std::string& script_source,
const std::string& expected_json_arguments_single_quotes,
bool expect_async_handler) {
return ExpectPass(MainContext(), object, script_source,
expected_json_arguments_single_quotes,
expect_async_handler);
}
v8::Local<v8::Value> ExpectPass(
v8::Local<v8::Context> context,
v8::Local<v8::Object> object,
const std::string& script_source,
const std::string& expected_json_arguments_single_quotes,
bool expect_async_handler) {
return RunTest(context, object, script_source, true,
ReplaceSingleQuotes(expected_json_arguments_single_quotes),
expect_async_handler, std::string());
}
void ExpectFailure(v8::Local<v8::Object> object,
const std::string& script_source,
const std::string& expected_error) {
RunTest(MainContext(), object, script_source, false, std::string(), false,
"Uncaught TypeError: " + expected_error);
}
void ExpectThrow(v8::Local<v8::Object> object,
const std::string& script_source,
const std::string& expected_error) {
RunTest(MainContext(), object, script_source, false, std::string(), false,
"Uncaught Error: " + expected_error);
}
bool HandlerWasInvoked() const { return last_request_ != nullptr; }
const APIRequestHandler::Request* last_request() const {
return last_request_.get();
}
void reset_last_request() { last_request_.reset(); }
const std::vector<std::string>& console_errors() const {
return console_errors_;
}
APIBinding* binding() { return binding_.get(); }
APIEventHandler* event_handler() { return event_handler_.get(); }
APIRequestHandler* request_handler() { return request_handler_.get(); }
const APITypeReferenceMap& type_refs() const { return type_refs_; }
private:
v8::Local<v8::Value> RunTest(v8::Local<v8::Context> context,
v8::Local<v8::Object> object,
const std::string& script_source,
bool should_pass,
const std::string& expected_json_arguments,
bool expect_async_handler,
const std::string& expected_error);
std::unique_ptr<APIRequestHandler::Request> last_request_;
std::vector<std::string> console_errors_;
GetParentCallback get_last_error_parent_;
std::unique_ptr<APIBinding> binding_;
std::unique_ptr<APIEventHandler> event_handler_;
std::unique_ptr<TestInteractionProvider> interaction_provider_;
std::unique_ptr<ExceptionHandler> exception_handler_;
std::unique_ptr<APIRequestHandler> request_handler_;
std::unique_ptr<BindingAccessChecker> access_checker_;
APITypeReferenceMap type_refs_;
base::Value::List binding_functions_;
base::Value::List binding_events_;
base::Value::List binding_types_;
base::Value::Dict binding_properties_;
std::unique_ptr<APIBindingHooks> binding_hooks_;
std::unique_ptr<APIBindingHooksDelegate> binding_hooks_delegate_;
APIBinding::CreateCustomType create_custom_type_;
APIBinding::OnSilentRequest on_silent_request_;
BindingAccessChecker::APIAvailabilityCallback api_availability_callback_;
BindingAccessChecker::PromiseAvailabilityCallback
promise_availability_callback_;
};
using APIBindingDeathTest = APIBindingUnittest;
v8::Local<v8::Value> APIBindingUnittest::RunTest(
v8::Local<v8::Context> context,
v8::Local<v8::Object> object,
const std::string& script_source,
bool should_pass,
const std::string& expected_json_arguments,
bool expect_async_handler,
const std::string& expected_error) {
EXPECT_FALSE(last_request_);
std::string wrapped_script_source =
base::StringPrintf("(function(obj) { %s })", script_source.c_str());
v8::Local<v8::Function> func =
FunctionFromString(context, wrapped_script_source);
if (func.IsEmpty()) {
ADD_FAILURE() << "Script source couldn't be converted to a function: "
<< script_source;
return v8::Local<v8::Value>();
}
v8::Local<v8::Value> argv[] = {object};
v8::Local<v8::Value> result;
if (should_pass) {
result = RunFunction(func, context, 1, argv);
if (!last_request_) {
ADD_FAILURE() << "No request was made. Script source: " << script_source;
return v8::Local<v8::Value>();
}
EXPECT_EQ(expected_json_arguments,
ValueToString(last_request_->arguments_list));
EXPECT_EQ(expect_async_handler, last_request_->has_async_response_handler)
<< script_source;
} else {
RunFunctionAndExpectError(func, context, 1, argv, expected_error);
EXPECT_FALSE(last_request_);
}
last_request_.reset();
return result;
}
TEST_F(APIBindingUnittest, TestEmptyAPI) {
InitializeBinding();
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
v8::Local<v8::Object> binding_object = binding()->CreateInstance(context);
EXPECT_EQ(
0u,
binding_object->GetOwnPropertyNames(context).ToLocalChecked()->Length());
}
// Tests the basic call -> request flow of the API binding (ensuring that
// functions are set up correctly and correctly enforced).
TEST_F(APIBindingUnittest, TestBasicAPICalls) {
SetFunctions(kFunctions);
InitializeBinding();
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
v8::Local<v8::Object> binding_object = binding()->CreateInstance(context);
// Argument parsing is tested primarily in APISignature and ArgumentSpec
// tests, so do a few quick sanity checks...
ExpectPass(binding_object, "obj.oneString('foo');", "['foo']", false);
ExpectFailure(binding_object, "obj.oneString(1);",
api_errors::InvocationError("test.oneString", "string str",
api_errors::NoMatchingSignature()));
ExpectPass(binding_object, "obj.stringAndInt('foo', 1)", "['foo',1]", false);
ExpectFailure(binding_object, "obj.stringAndInt(1)",
api_errors::InvocationError("test.stringAndInt",
"string str, integer int",
api_errors::NoMatchingSignature()));
ExpectPass(binding_object, "obj.intAndCallback(1, function() {})", "[1]",
true);
ExpectFailure(binding_object, "obj.intAndCallback(function() {})",
api_errors::InvocationError("test.intAndCallback",
"integer int, function callback",
api_errors::NoMatchingSignature()));
// ...And an interesting case (throwing an error during parsing).
ExpectThrow(binding_object,
"obj.oneObject({ get prop1() { throw new Error('Badness'); } });",
"Badness");
}
// Test that enum values are properly exposed on the binding object.
TEST_F(APIBindingUnittest, EnumValues) {
const char kTypes[] =
"[{"
" 'id': 'first',"
" 'type': 'string',"
" 'enum': ['alpha', 'camelCase', 'Hyphen-ated',"
" 'SCREAMING', 'nums123', '42nums']"
"}, {"
" 'id': 'last',"
" 'type': 'string',"
" 'enum': [{'name': 'omega'}]"
"}]";
SetTypes(kTypes);
InitializeBinding();
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
v8::Local<v8::Object> binding_object = binding()->CreateInstance(context);
const char kExpected[] =
"{'ALPHA':'alpha','CAMEL_CASE':'camelCase','HYPHEN_ATED':'Hyphen-ated',"
"'NUMS123':'nums123','SCREAMING':'SCREAMING','_42NUMS':'42nums'}";
EXPECT_EQ(ReplaceSingleQuotes(kExpected),
GetStringPropertyFromObject(binding_object, context, "first"));
EXPECT_EQ(ReplaceSingleQuotes("{'OMEGA':'omega'}"),
GetStringPropertyFromObject(binding_object, context, "last"));
}
// Test that empty enum entries are (unfortunately) allowed.
TEST_F(APIBindingUnittest, EnumWithEmptyEntry) {
const char kTypes[] =
"[{"
" 'id': 'enumWithEmpty',"
" 'type': 'string',"
" 'enum': [{'name': ''}, {'name': 'other'}]"
"}]";
SetTypes(kTypes);
InitializeBinding();
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
v8::Local<v8::Object> binding_object = binding()->CreateInstance(context);
EXPECT_EQ(
"{\"\":\"\",\"OTHER\":\"other\"}",
GetStringPropertyFromObject(binding_object, context, "enumWithEmpty"));
}
// Test that type references are correctly set up in the API.
TEST_F(APIBindingUnittest, TypeRefsTest) {
const char kTypes[] =
"[{"
" 'id': 'refObj',"
" 'type': 'object',"
" 'properties': {"
" 'prop1': {'type': 'string'},"
" 'prop2': {'type': 'integer', 'optional': true}"
" }"
"}, {"
" 'id': 'refEnum',"
" 'type': 'string',"
" 'enum': ['alpha', 'beta']"
"}]";
const char kRefFunctions[] =
"[{"
" 'name': 'takesRefObj',"
" 'parameters': [{"
" 'name': 'o',"
" '$ref': 'refObj'"
" }]"
"}, {"
" 'name': 'takesRefEnum',"
" 'parameters': [{"
" 'name': 'e',"
" '$ref': 'refEnum'"
" }]"
"}]";
SetFunctions(kRefFunctions);
SetTypes(kTypes);
InitializeBinding();
EXPECT_EQ(2u, type_refs().size());
EXPECT_TRUE(type_refs().GetSpec("refObj"));
EXPECT_TRUE(type_refs().GetSpec("refEnum"));
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
v8::Local<v8::Object> binding_object = binding()->CreateInstance(context);
// Parsing in general is tested in APISignature and ArgumentSpec tests, but
// we test that the binding a) correctly finds the definitions, and b) accepts
// properties from the API object.
ExpectPass(binding_object, "obj.takesRefObj({prop1: 'foo'})",
"[{'prop1':'foo'}]", false);
ExpectFailure(binding_object, "obj.takesRefObj({prop1: 'foo', prop2: 'a'})",
api_errors::InvocationError(
"test.takesRefObj", "refObj o",
api_errors::ArgumentError(
"o", api_errors::PropertyError(
"prop2", api_errors::InvalidType(
api_errors::kTypeInteger,
api_errors::kTypeString)))));
ExpectPass(binding_object, "obj.takesRefEnum('alpha')", "['alpha']", false);
ExpectPass(binding_object, "obj.takesRefEnum(obj.refEnum.BETA)", "['beta']",
false);
ExpectFailure(binding_object, "obj.takesRefEnum('gamma')",
api_errors::InvocationError(
"test.takesRefEnum", "refEnum e",
api_errors::ArgumentError(
"e", api_errors::InvalidEnumValue({"alpha", "beta"}))));
}
TEST_F(APIBindingUnittest, RestrictedAPIs) {
const char kLocalFunctions[] =
"[{"
" 'name': 'allowedOne',"
" 'parameters': []"
"}, {"
" 'name': 'allowedTwo',"
" 'parameters': []"
"}, {"
" 'name': 'restrictedOne',"
" 'parameters': []"
"}, {"
" 'name': 'restrictedTwo',"
" 'parameters': []"
"}]";
SetFunctions(kLocalFunctions);
const char kEvents[] =
"[{'name': 'allowedEvent'}, {'name': 'restrictedEvent'}]";
SetEvents(kEvents);
const char kProperties[] =
R"({
"allowedProperty": { "type": "integer", "value": 3 },
"restrictedProperty": { "type": "string", "value": "restricted" }
})";
SetProperties(kProperties);
auto is_available = [](v8::Local<v8::Context> context,
const std::string& name) {
std::set<std::string> allowed = {"test.allowedOne", "test.allowedTwo",
"test.allowedEvent",
"test.allowedProperty"};
std::set<std::string> restricted = {
"test.restrictedOne", "test.restrictedTwo", "test.restrictedEvent",
"test.restrictedProperty"};
EXPECT_TRUE(allowed.count(name) || restricted.count(name)) << name;
return allowed.count(name) != 0;
};
SetAPIAvailabilityCallback(base::BindRepeating(is_available));
InitializeBinding();
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
v8::Local<v8::Object> binding_object = binding()->CreateInstance(context);
auto is_defined = [&binding_object, context](const std::string& name) {
v8::Local<v8::Value> val =
GetPropertyFromObject(binding_object, context, name);
EXPECT_FALSE(val.IsEmpty());
return !val->IsUndefined() && !val->IsNull();
};
EXPECT_TRUE(is_defined("allowedOne"));
EXPECT_TRUE(is_defined("allowedTwo"));
EXPECT_TRUE(is_defined("allowedEvent"));
EXPECT_TRUE(is_defined("allowedProperty"));
EXPECT_FALSE(is_defined("restrictedOne"));
EXPECT_FALSE(is_defined("restrictedTwo"));
EXPECT_FALSE(is_defined("restrictedEvent"));
EXPECT_FALSE(is_defined("restrictedProperty"));
}
// Tests that events specified in the API are created as properties of the API
// object.
TEST_F(APIBindingUnittest, TestEventCreation) {
SetEvents(
R"([
{'name': 'onFoo'},
{'name': 'onBar'},
{'name': 'onBaz', 'options': {'maxListeners': 1}}
])");
SetFunctions(kFunctions);
InitializeBinding();
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
v8::Local<v8::Object> binding_object = binding()->CreateInstance(context);
// Event behavior is tested in the APIEventHandler unittests as well as the
// APIBindingsSystem tests, so we really only need to check that the events
// are being initialized on the object.
v8::Maybe<bool> has_on_foo =
binding_object->Has(context, gin::StringToV8(isolate(), "onFoo"));
EXPECT_TRUE(has_on_foo.IsJust());
EXPECT_TRUE(has_on_foo.FromJust());
v8::Maybe<bool> has_on_bar =
binding_object->Has(context, gin::StringToV8(isolate(), "onBar"));
EXPECT_TRUE(has_on_bar.IsJust());
EXPECT_TRUE(has_on_bar.FromJust());
v8::Maybe<bool> has_on_baz =
binding_object->Has(context, gin::StringToV8(isolate(), "onBaz"));
EXPECT_TRUE(has_on_baz.IsJust());
EXPECT_TRUE(has_on_baz.FromJust());
// Test that the maxListeners property is correctly used.
v8::Local<v8::Function> add_listener = FunctionFromString(
context, "(function(e) { e.addListener(function() {}); })");
v8::Local<v8::Value> args[] = {
GetPropertyFromObject(binding_object, context, "onBaz")};
RunFunction(add_listener, context, std::size(args), args);
EXPECT_EQ(1u, event_handler()->GetNumEventListenersForTesting("test.onBaz",
context));
RunFunctionAndExpectError(add_listener, context, std::size(args), args,
"Uncaught TypeError: Too many listeners.");
EXPECT_EQ(1u, event_handler()->GetNumEventListenersForTesting("test.onBaz",
context));
v8::Maybe<bool> has_nonexistent_event = binding_object->Has(
context, gin::StringToV8(isolate(), "onNonexistentEvent"));
EXPECT_TRUE(has_nonexistent_event.IsJust());
EXPECT_FALSE(has_nonexistent_event.FromJust());
}
TEST_F(APIBindingUnittest, TestProperties) {
SetProperties(
"{"
" 'prop1': { 'value': 17, 'type': 'integer' },"
" 'prop2': {"
" 'type': 'object',"
" 'properties': {"
" 'subprop1': { 'value': 'some value', 'type': 'string' },"
" 'subprop2': { 'value': true, 'type': 'boolean' }"
" }"
" },"
" 'linuxOnly': {"
" 'value': 'linux',"
" 'type': 'string',"
" 'platforms': ['linux']"
" },"
" 'notLinux': {"
" 'value': 'nonlinux',"
" 'type': 'string',"
" 'platforms': ['win', 'mac', 'chromeos', 'desktop_android']"
" }"
"}");
InitializeBinding();
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
v8::Local<v8::Object> binding_object = binding()->CreateInstance(context);
EXPECT_EQ("17",
GetStringPropertyFromObject(binding_object, context, "prop1"));
EXPECT_EQ(R"({"subprop1":"some value","subprop2":true})",
GetStringPropertyFromObject(binding_object, context, "prop2"));
#if BUILDFLAG(IS_LINUX)
EXPECT_EQ("\"linux\"",
GetStringPropertyFromObject(binding_object, context, "linuxOnly"));
EXPECT_EQ("undefined",
GetStringPropertyFromObject(binding_object, context, "notLinux"));
#else
EXPECT_EQ("undefined",
GetStringPropertyFromObject(binding_object, context, "linuxOnly"));
EXPECT_EQ("\"nonlinux\"",
GetStringPropertyFromObject(binding_object, context, "notLinux"));
#endif
}
TEST_F(APIBindingUnittest, TestRefProperties) {
SetProperties(
"{"
" 'alpha': {"
" '$ref': 'AlphaRef',"
" 'value': ['a']"
" },"
" 'beta': {"
" '$ref': 'BetaRef',"
" 'value': ['b']"
" }"
"}");
auto create_custom_type = [](v8::Isolate* isolate,
const std::string& type_name,
const std::string& property_name,
const base::Value::List* property_values) {
v8::Local<v8::Context> context = isolate->GetCurrentContext();
v8::Local<v8::Object> result = v8::Object::New(isolate);
if (type_name == "AlphaRef") {
EXPECT_EQ("alpha", property_name);
EXPECT_EQ("[\"a\"]", ValueToString(*property_values));
result
->Set(context, gin::StringToSymbol(isolate, "alphaProp"),
gin::StringToV8(isolate, "alphaVal"))
.ToChecked();
} else if (type_name == "BetaRef") {
EXPECT_EQ("beta", property_name);
EXPECT_EQ("[\"b\"]", ValueToString(*property_values));
result
->Set(context, gin::StringToSymbol(isolate, "betaProp"),
gin::StringToV8(isolate, "betaVal"))
.ToChecked();
} else {
EXPECT_TRUE(false) << type_name;
}
return result;
};
SetCreateCustomType(base::BindRepeating(create_custom_type));
InitializeBinding();
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
v8::Local<v8::Object> binding_object = binding()->CreateInstance(context);
EXPECT_EQ(R"({"alphaProp":"alphaVal"})",
GetStringPropertyFromObject(binding_object, context, "alpha"));
EXPECT_EQ(
R"({"betaProp":"betaVal"})",
GetStringPropertyFromObject(binding_object, context, "beta"));
}
TEST_F(APIBindingUnittest, TestDisposedContext) {
SetFunctions(kFunctions);
InitializeBinding();
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
v8::Local<v8::Object> binding_object = binding()->CreateInstance(context);
v8::Local<v8::Function> func =
FunctionFromString(context, "(function(obj) { obj.oneString('foo'); })");
v8::Local<v8::Value> argv[] = {binding_object};
DisposeContext(context);
RunFunctionAndExpectError(func, context, std::size(argv), argv,
"Uncaught Error: Extension context invalidated.");
EXPECT_FALSE(HandlerWasInvoked());
// This test passes if this does not crash, even under AddressSanitizer
// builds.
}
TEST_F(APIBindingUnittest, TestInvalidatedContext) {
SetFunctions(kFunctions);
InitializeBinding();
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
v8::Local<v8::Object> binding_object = binding()->CreateInstance(context);
v8::Local<v8::Function> func =
FunctionFromString(context, "(function(obj) { obj.oneString('foo'); })");
v8::Local<v8::Value> argv[] = {binding_object};
binding::InvalidateContext(context);
RunFunctionAndExpectError(func, context, std::size(argv), argv,
"Uncaught Error: Extension context invalidated.");
EXPECT_FALSE(HandlerWasInvoked());
// This test passes if this does not crash, even under AddressSanitizer
// builds.
}
TEST_F(APIBindingUnittest, MultipleContexts) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context_a = MainContext();
v8::Local<v8::Context> context_b = AddContext();
SetFunctions(kFunctions);
InitializeBinding();
v8::Local<v8::Object> binding_object_a = binding()->CreateInstance(context_a);
v8::Local<v8::Object> binding_object_b = binding()->CreateInstance(context_b);
ExpectPass(context_a, binding_object_a, "obj.oneString('foo');", "['foo']",
false);
ExpectPass(context_b, binding_object_b, "obj.oneString('foo');", "['foo']",
false);
DisposeContext(context_b);
ExpectPass(context_a, binding_object_a, "obj.oneString('foo');", "['foo']",
false);
}
// Tests adding custom hooks for an API method.
TEST_F(APIBindingUnittest, TestCustomHooks) {
SetFunctions(kFunctions);
// Register a hook for the test.oneString method.
auto hooks = std::make_unique<APIBindingHooksTestDelegate>();
bool did_call = false;
auto hook = [](bool* did_call, const APISignature* signature,
v8::Local<v8::Context> context,
v8::LocalVector<v8::Value>* arguments,
const APITypeReferenceMap& ref_map) {
*did_call = true;
APIBindingHooks::RequestResult result(
APIBindingHooks::RequestResult::HANDLED);
if (arguments->size() != 1u) { // ASSERT* messes with the return type.
EXPECT_EQ(1u, arguments->size());
return result;
}
EXPECT_EQ("foo",
gin::V8ToString(v8::Isolate::GetCurrent(), arguments->at(0)));
return result;
};
hooks->AddHandler("test.oneString", base::BindRepeating(hook, &did_call));
SetHooksDelegate(std::move(hooks));
InitializeBinding();
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
v8::Local<v8::Object> binding_object = binding()->CreateInstance(context);
// First try calling the oneString() method, which has a custom hook
// installed.
v8::Local<v8::Function> func =
FunctionFromString(context, "(function(obj) { obj.oneString('foo'); })");
v8::Local<v8::Value> args[] = {binding_object};
RunFunction(func, context, 1, args);
EXPECT_TRUE(did_call);
// Other methods, like stringAndInt(), should behave normally.
ExpectPass(binding_object, "obj.stringAndInt('foo', 42);", "['foo',42]",
false);
}
TEST_F(APIBindingUnittest, TestJSCustomHook) {
// Register a hook for the test.oneString method.
const char kRegisterHook[] = R"(
(function(hooks) {
hooks.setHandleRequest('oneString', function() {
this.requestArguments = Array.from(arguments);
});
}))";
InitializeJSHooks(kRegisterHook);
SetFunctions(kFunctions);
InitializeBinding();
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
v8::Local<v8::Object> binding_object = binding()->CreateInstance(context);
// First try calling with an invalid invocation. An error should be raised and
// the hook should never have been called, since the arguments didn't match.
ExpectFailure(binding_object, "obj.oneString(1);",
api_errors::InvocationError("test.oneString", "string str",
api_errors::NoMatchingSignature()));
v8::Local<v8::Value> property =
GetPropertyFromObject(context->Global(), context, "requestArguments");
ASSERT_FALSE(property.IsEmpty());
EXPECT_TRUE(property->IsUndefined());
// Try calling the oneString() method with valid arguments. The hook should
// be called.
v8::Local<v8::Function> func =
FunctionFromString(context, "(function(obj) { obj.oneString('foo'); })");
v8::Local<v8::Value> args[] = {binding_object};
RunFunction(func, context, 1, args);
EXPECT_EQ("[\"foo\"]", GetStringPropertyFromObject(
context->Global(), context, "requestArguments"));
// Other methods, like stringAndInt(), should behave normally.
ExpectPass(binding_object, "obj.stringAndInt('foo', 42);", "['foo',42]",
false);
}
// Tests the updateArgumentsPreValidate hook.
TEST_F(APIBindingUnittest, TestUpdateArgumentsPreValidate) {
// Register a hook for the test.oneString method.
const char kRegisterHook[] = R"(
(function(hooks) {
hooks.setUpdateArgumentsPreValidate('oneString', function() {
this.requestArguments = Array.from(arguments);
if (this.requestArguments[0] === true)
return ['hooked']
return this.requestArguments
});
}))";
InitializeJSHooks(kRegisterHook);
SetFunctions(kFunctions);
InitializeBinding();
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
v8::Local<v8::Object> binding_object = binding()->CreateInstance(context);
// Call the method with a hook. Since the hook updates arguments before
// validation, we should be able to pass in invalid arguments and still
// have the hook called.
ExpectFailure(binding_object, "obj.oneString(false);",
api_errors::InvocationError("test.oneString", "string str",
api_errors::NoMatchingSignature()));
EXPECT_EQ("[false]", GetStringPropertyFromObject(
context->Global(), context, "requestArguments"));
ExpectPass(binding_object, "obj.oneString(true);", "['hooked']", false);
EXPECT_EQ("[true]", GetStringPropertyFromObject(
context->Global(), context, "requestArguments"));
// Other methods, like stringAndInt(), should behave normally.
ExpectPass(binding_object, "obj.stringAndInt('foo', 42);", "['foo',42]",
false);
}
// Tests the updateArgumentsPreValidate hook.
TEST_F(APIBindingUnittest, TestThrowInUpdateArgumentsPreValidate) {
// Register a hook for the test.oneString method.
const char kRegisterHook[] = R"(
(function(hooks) {
hooks.setUpdateArgumentsPreValidate('oneString', function() {
throw new Error('Custom Hook Error');
});
}))";
InitializeJSHooks(kRegisterHook);
SetFunctions(kFunctions);
InitializeBinding();
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
v8::Local<v8::Object> binding_object = binding()->CreateInstance(context);
v8::Local<v8::Function> function =
FunctionFromString(context,
"(function(obj) { return obj.oneString('ping'); })");
v8::Local<v8::Value> args[] = {binding_object};
{
TestJSRunner::AllowErrors allow_errors;
RunFunctionAndExpectError(function, context, v8::Undefined(isolate()),
std::size(args), args,
"Uncaught Error: Custom Hook Error");
}
// Other methods, like stringAndInt(), should behave normally.
ExpectPass(binding_object, "obj.stringAndInt('foo', 42);", "['foo',42]",
false);
}
// Tests that custom JS hooks can return results synchronously.
TEST_F(APIBindingUnittest, TestReturningResultFromCustomJSHook) {
// Register a hook for the test.oneString method.
const char kRegisterHook[] = R"(
(function(hooks) {
hooks.setHandleRequest('oneString', str => {
return str + ' pong';
});
}))";
InitializeJSHooks(kRegisterHook);
SetFunctions(kFunctions);
InitializeBinding();
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
v8::Local<v8::Object> binding_object = binding()->CreateInstance(context);
v8::Local<v8::Function> function =
FunctionFromString(context,
"(function(obj) { return obj.oneString('ping'); })");
v8::Local<v8::Value> args[] = {binding_object};
v8::Local<v8::Value> result =
RunFunction(function, context, std::size(args), args);
ASSERT_FALSE(result.IsEmpty());
std::unique_ptr<base::Value> json_result = V8ToBaseValue(result, context);
ASSERT_TRUE(json_result);
EXPECT_EQ("\"ping pong\"", ValueToString(*json_result));
}
// Tests that the setHandleRequest hook can use callbacks and promises.
TEST_F(APIBindingUnittest, TestReturningPromiseFromHandleRequestHook) {
bool context_allows_promises = true;
SetPromiseAvailabilityFlag(&context_allows_promises);
// Register a hook for supportsPromises.
const char kRegisterHook[] = R"(
(function(hooks) {
hooks.setHandleRequest('supportsPromises', (firstArg, callback) => {
this.firstArgument = firstArg;
this.secondArgument = callback;
});
}))";
InitializeJSHooks(kRegisterHook);
SetFunctions(kFunctionsWithPromiseSignatures);
InitializeBinding();
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
v8::Local<v8::Object> binding_object = binding()->CreateInstance(context);
{
// Calling supportsPromises normally with a callback should work fine and
// the callback should be invoked immediately.
const char kFunctionCall[] =
R"((function(obj) {
return obj.supportsPromises(5, (arg) => {
this.sentToCallback = arg;
});
}))";
v8::Local<v8::Function> function =
FunctionFromString(context, kFunctionCall);
v8::Local<v8::Value> args[] = {binding_object};
auto result = RunFunction(function, context, v8::Undefined(isolate()),
std::size(args), args);
ASSERT_FALSE(result.IsEmpty());
EXPECT_TRUE(result->IsUndefined());
EXPECT_EQ("5", GetStringPropertyFromObject(context->Global(), context,
"firstArgument"));
v8::Local<v8::Function> resolve_callback;
ASSERT_TRUE(GetPropertyFromObjectAs(context->Global(), context,
"secondArgument", &resolve_callback));
// The callback arg will not be set until the callback has been invoked.
EXPECT_TRUE(
GetPropertyFromObject(context->Global(), context, "sentToCallabck")
->IsUndefined());
v8::Local<v8::Value> callback_arguments[] = {
gin::StringToV8(isolate(), "foo")};
RunFunctionOnGlobal(resolve_callback, context,
std::size(callback_arguments), callback_arguments);
EXPECT_EQ(R"("foo")", GetStringPropertyFromObject(
context->Global(), context, "sentToCallback"));
}
{
// Calling supportsPromises normally without the callback should work fine
// and a promise should be returned that is resolved when the callback is
// invoked.
v8::Local<v8::Function> function = FunctionFromString(
context, "(function(obj) { return obj.supportsPromises(6); })");
v8::Local<v8::Value> args[] = {binding_object};
v8::Local<v8::Value> result = RunFunction(
function, context, v8::Undefined(isolate()), std::size(args), args);
v8::Local<v8::Promise> promise;
ASSERT_TRUE(GetValueAs(result, &promise));
EXPECT_EQ(v8::Promise::kPending, promise->State());
EXPECT_EQ("6", GetStringPropertyFromObject(context->Global(), context,
"firstArgument"));
// Since we trigger the promise to be resolved with a function that calls
// back into the C++ side, the second argument is actually a function here.
v8::Local<v8::Function> resolve_callback;
ASSERT_TRUE(GetPropertyFromObjectAs(context->Global(), context,
"secondArgument", &resolve_callback));
// Invoking this callback should result in the promise being resolved.
v8::Local<v8::Value> callback_arguments[] = {
gin::StringToV8(isolate(), "bar")};
RunFunctionOnGlobal(resolve_callback, context,
std::size(callback_arguments), callback_arguments);
EXPECT_EQ(v8::Promise::kFulfilled, promise->State());
EXPECT_EQ(R"("bar")", V8ToString(promise->Result(), context));
}
{
// If the context doesn't support promises, there should be an error if a
// required callback isn't supplied.
context_allows_promises = false;
v8::Local<v8::Function> function = FunctionFromString(
context, "(function(obj) { return obj.supportsPromises(7); })");
v8::Local<v8::Value> args[] = {binding_object};
auto expected_error =
"Uncaught TypeError: " +
api_errors::InvocationError("test.supportsPromises",
"integer int, function callback",
api_errors::NoMatchingSignature());
RunFunctionAndExpectError(function, context, std::size(args), args,
expected_error);
}
}
// Tests that JS custom hooks can throw exceptions for bad invocations.
TEST_F(APIBindingUnittest, TestThrowingFromCustomJSHook) {
// Register a hook for the test.oneString method.
const char kRegisterHook[] = R"(
(function(hooks) {
hooks.setHandleRequest('oneString', str => {
throw new Error('Custom Hook Error');
});
}))";
InitializeJSHooks(kRegisterHook);
SetFunctions(kFunctions);
InitializeBinding();
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
v8::Local<v8::Object> binding_object = binding()->CreateInstance(context);
v8::Local<v8::Function> function =
FunctionFromString(context,
"(function(obj) { return obj.oneString('ping'); })");
v8::Local<v8::Value> args[] = {binding_object};
TestJSRunner::AllowErrors allow_errors;
RunFunctionAndExpectError(function, context, v8::Undefined(isolate()),
std::size(args), args,
"Uncaught Error: Custom Hook Error");
}
// Tests that JS setHandleRequestHooks can use the failure callback to return a
// failure result for an API.
TEST_F(APIBindingUnittest, TestHandleRequestFailureCallback) {
bool context_allows_promises = true;
SetPromiseAvailabilityFlag(&context_allows_promises);
// Register a hook for supportsPromises that calls the failure callback when
// the API is called with the integer 6.
const char kRegisterHook[] = R"(
(function(hooks) {
function handler(firstArg, callback, failureCallback) {
if (firstArg == 6)
failureCallback('This is the error');
else
callback(firstArg);
};
hooks.setHandleRequest('supportsPromises', handler);
hooks.setHandleRequest('callbackOptional', handler);
}))";
InitializeJSHooks(kRegisterHook);
SetFunctions(kFunctionsWithPromiseSignatures);
InitializeBinding();
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
v8::Local<v8::Object> binding_object = binding()->CreateInstance(context);
v8::Local<v8::Object> last_error_parent = v8::Object::New(isolate());
auto get_last_error_parent = [&last_error_parent]() {
return last_error_parent;
};
SetLastErrorParentCallback(base::BindLambdaForTesting(get_last_error_parent));
{
// Calling supportsPromises normally should resolve as expected with no
// error.
v8::Local<v8::Function> function = FunctionFromString(
context, "(function(obj) { return obj.supportsPromises(42); })");
v8::Local<v8::Value> args[] = {binding_object};
v8::Local<v8::Value> result = RunFunction(
function, context, v8::Undefined(isolate()), std::size(args), args);
v8::Local<v8::Promise> promise;
ASSERT_TRUE(GetValueAs(result, &promise));
EXPECT_EQ(v8::Promise::kFulfilled, promise->State());
EXPECT_EQ(R"(42)", V8ToString(promise->Result(), context));
}
{
// Calling supportsPromises to trigger the failureCallback should result in
// the promise being rejected.
v8::Local<v8::Function> function = FunctionFromString(
context, "(function(obj) { return obj.supportsPromises(6); })");
v8::Local<v8::Value> args[] = {binding_object};
v8::Local<v8::Value> result = RunFunction(
function, context, v8::Undefined(isolate()), std::size(args), args);
v8::Local<v8::Promise> promise;
ASSERT_TRUE(GetValueAs(result, &promise));
EXPECT_EQ(v8::Promise::kRejected, promise->State());
ASSERT_TRUE(promise->Result()->IsObject());
EXPECT_EQ(R"("This is the error")",
GetStringPropertyFromObject(promise->Result().As<v8::Object>(),
context, "message"));
}
{
// Calling supportsPromises with a callback and triggering the
// failureCallback should call the callback with lastError set.
const char kFunctionCall[] =
R"((function(obj, lastErrorParent) {
return obj.supportsPromises(6, (arg) => {
this.sentToCallback = arg;
// LastError is only set for the duration of the callback, so set
// it to a global we retrieve and can check later.
this.lastError = lastErrorParent.lastError;
});
}))";
v8::Local<v8::Function> function =
FunctionFromString(context, kFunctionCall);
v8::Local<v8::Value> args[] = {binding_object, last_error_parent};
RunFunction(function, context, v8::Undefined(isolate()), std::size(args),
args);
// In the case of errors, callbacks are not passed any arguments.
EXPECT_TRUE(
GetPropertyFromObject(context->Global(), context, "sentToCallabck")
->IsUndefined());
v8::Local<v8::Object> last_error;
ASSERT_TRUE(GetPropertyFromObjectAs(context->Global(), context, "lastError",
&last_error));
EXPECT_EQ(R"("This is the error")",
GetStringPropertyFromObject(last_error, context, "message"));
}
// Set the context to not support promises for the following test cases.
context_allows_promises = false;
{
// Calling callbackOptional without a callback and triggering the
// failureCallback in a context that does not support promises should result
// in a console error about an unchecked last error.
const char kFunctionCall[] =
R"((function(obj) {
return obj.callbackOptional(6);
}))";
v8::Local<v8::Function> function =
FunctionFromString(context, kFunctionCall);
v8::Local<v8::Value> args[] = {binding_object, last_error_parent};
RunFunction(function, context, v8::Undefined(isolate()), std::size(args),
args);
ASSERT_EQ(1u, console_errors().size());
EXPECT_THAT(console_errors()[0],
"Unchecked runtime.lastError: This is the error");
// Clear the console errors in case any other test case uses them.
ClearConsoleErrors();
}
}
// Tests that a JS handle request hook that calls the resolver callback more
// than once will fail gracefully on a release build. Regression test for
// https://crbug.com/1298409.
TEST_F(APIBindingUnittest, TestHandleRequestHookCalledTwiceGracefulRegression) {
bool context_allows_promises = true;
SetPromiseAvailabilityFlag(&context_allows_promises);
// Register a hook for supportsPromises that calls the success callback twice.
static const char* const kRegisterHook = R"(
(function(hooks) {
function handler(firstArg, callback, failureCallback) {
callback(firstArg);
// Calling the callback to resolve the request a second time is
// something our custom hooks shouldn't be doing, but this test
// intentionally does it to verify behavior if it does happen by
// accident.
callback(firstArg);
};
hooks.setHandleRequest('supportsPromises', handler);
}))";
InitializeJSHooks(kRegisterHook);
SetFunctions(kFunctionsWithPromiseSignatures);
InitializeBinding();
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
v8::Local<v8::Object> binding_object = binding()->CreateInstance(context);
v8::Local<v8::Function> function = FunctionFromString(
context, "(function(obj) { return obj.supportsPromises(42); })");
v8::Local<v8::Value> args[] = {binding_object};
// Calling supportsPromises will trigger the HandleRequest hook which attempts
// to resolve the request twice by calling the success callback twice. This
// should gracefully fail without a crash and still result in the request
// resolving as expected.
v8::Local<v8::Value> result = RunFunction(
function, context, v8::Undefined(isolate()), std::size(args), args);
v8::Local<v8::Promise> promise;
ASSERT_TRUE(GetValueAs(result, &promise));
EXPECT_EQ(v8::Promise::kFulfilled, promise->State());
EXPECT_EQ(R"(42)", V8ToString(promise->Result(), context));
}
// Tests that JS custom hooks correctly handle the context being invalidated.
// Regression test for https://crbug.com/944014.
TEST_F(APIBindingUnittest, TestInvalidatingInCustomHook) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
auto context_invalidator =
[](const v8::FunctionCallbackInfo<v8::Value>& info) {
gin::Arguments arguments(info);
binding::InvalidateContext(arguments.GetHolderCreationContext());
};
v8::Local<v8::Function> v8_context_invalidator =
v8::Function::New(context, context_invalidator).ToLocalChecked();
// Register two hooks. Since the context is invalidated in the first, the
// second should never run.
const char kRegisterHook[] = R"(
(function(hooks, contextInvalidator) {
hooks.setUpdateArgumentsPreValidate('oneString', () => {
contextInvalidator();
return ['foo'];
});
hooks.setHandleRequest('oneString', () => {
this.ranHandleHook = true;
});
}))";
InitializeJSHooks(kRegisterHook, v8_context_invalidator);
SetFunctions(kFunctions);
InitializeBinding();
v8::Local<v8::Object> binding_object = binding()->CreateInstance(context);
v8::Local<v8::Function> function = FunctionFromString(
context, "(function(obj) { return obj.oneString('ping'); })");
v8::Local<v8::Value> args[] = {binding_object};
RunFunction(function, context, v8::Undefined(isolate()), std::size(args),
args);
// The context should be properly invalidated, and the second hook (which
// sets "ranHandleHook") shouldn't have ran.
EXPECT_FALSE(binding::IsContextValid(context));
EXPECT_EQ("undefined", GetStringPropertyFromObject(context->Global(), context,
"ranHandleHook"));
}
// Tests that native custom hooks can return results synchronously, or throw
// exceptions for bad invocations.
TEST_F(APIBindingUnittest,
TestReturningResultAndThrowingExceptionFromCustomNativeHook) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
// Register a hook for the test.oneString method.
auto hooks = std::make_unique<APIBindingHooksTestDelegate>();
bool did_call = false;
auto hook = [](bool* did_call, const APISignature* signature,
v8::Local<v8::Context> context,
v8::LocalVector<v8::Value>* arguments,
const APITypeReferenceMap& ref_map) {
APIBindingHooks::RequestResult result(
APIBindingHooks::RequestResult::HANDLED);
if (arguments->size() != 1u) { // ASSERT* messes with the return type.
EXPECT_EQ(1u, arguments->size());
return result;
}
v8::Isolate* isolate = v8::Isolate::GetCurrent();
std::string arg_value = gin::V8ToString(isolate, arguments->at(0));
if (arg_value == "throw") {
isolate->ThrowException(v8::Exception::Error(
gin::StringToV8(isolate, "Custom Hook Error")));
result.code = APIBindingHooks::RequestResult::THROWN;
return result;
}
result.return_value = gin::StringToV8(isolate, arg_value + " pong");
return result;
};
hooks->AddHandler("test.oneString", base::BindRepeating(hook, &did_call));
SetHooksDelegate(std::move(hooks));
SetFunctions(kFunctions);
InitializeBinding();
v8::Local<v8::Object> binding_object = binding()->CreateInstance(context);
{
// Test an invocation that we expect to throw an exception.
v8::Local<v8::Function> function =
FunctionFromString(
context, "(function(obj) { return obj.oneString('throw'); })");
v8::Local<v8::Value> args[] = {binding_object};
RunFunctionAndExpectError(function, context, v8::Undefined(isolate()),
std::size(args), args,
"Uncaught Error: Custom Hook Error");
}
{
// Test an invocation we expect to succeed.
v8::Local<v8::Function> function =
FunctionFromString(context,
"(function(obj) { return obj.oneString('ping'); })");
v8::Local<v8::Value> args[] = {binding_object};
v8::Local<v8::Value> result =
RunFunction(function, context, std::size(args), args);
ASSERT_FALSE(result.IsEmpty());
std::unique_ptr<base::Value> json_result = V8ToBaseValue(result, context);
ASSERT_TRUE(json_result);
EXPECT_EQ("\"ping pong\"", ValueToString(*json_result));
}
}
// Tests the updateArgumentsPostValidate hook.
TEST_F(APIBindingUnittest, TestUpdateArgumentsPostValidate) {
// Register a hook for the test.oneString method.
const char kRegisterHook[] = R"(
(function(hooks) {
hooks.setUpdateArgumentsPostValidate('oneString', function() {
this.requestArguments = Array.from(arguments);
return ['pong'];
});
}))";
InitializeJSHooks(kRegisterHook);
SetFunctions(kFunctions);
InitializeBinding();
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
v8::Local<v8::Object> binding_object = binding()->CreateInstance(context);
// Try calling the method with an invalid signature. Since it's invalid, we
// should never enter the hook.
ExpectFailure(binding_object, "obj.oneString(false);",
api_errors::InvocationError("test.oneString", "string str",
api_errors::NoMatchingSignature()));
EXPECT_EQ("undefined", GetStringPropertyFromObject(
context->Global(), context, "requestArguments"));
// Call the method with a valid signature. The hook should be entered and
// manipulate the arguments.
ExpectPass(binding_object, "obj.oneString('ping');", "['pong']", false);
EXPECT_EQ("[\"ping\"]", GetStringPropertyFromObject(
context->Global(), context, "requestArguments"));
// Other methods, like stringAndInt(), should behave normally.
ExpectPass(binding_object, "obj.stringAndInt('foo', 42);",
"['foo',42]", false);
}
// Tests using setUpdateArgumentsPostValidate to return a list of arguments
// that violates the function schema. Sadly, this should succeed. :(
// See comment in api_binding.cc.
TEST_F(APIBindingUnittest, TestUpdateArgumentsPostValidateViolatingSchema) {
// Register a hook for the test.oneString method.
const char kRegisterHook[] = R"(
(function(hooks) {
hooks.setUpdateArgumentsPostValidate('oneString', function() {
return [{}];
});
}))";
InitializeJSHooks(kRegisterHook);
SetFunctions(kFunctions);
InitializeBinding();
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
v8::Local<v8::Object> binding_object = binding()->CreateInstance(context);
// Call the method with a valid signature. The hook should be entered and
// manipulate the arguments.
ExpectPass(binding_object, "obj.oneString('ping');", "[{}]", false);
}
// Test that user gestures are properly recorded when calling APIs.
TEST_F(APIBindingUnittest, TestUserGestures) {
SetFunctions(kFunctions);
InitializeBinding();
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
v8::Local<v8::Object> binding_object = binding()->CreateInstance(context);
v8::Local<v8::Function> function =
FunctionFromString(context, "(function(obj) { obj.oneString('foo');})");
ASSERT_FALSE(function.IsEmpty());
v8::Local<v8::Value> argv[] = {binding_object};
RunFunction(function, context, std::size(argv), argv);
ASSERT_TRUE(last_request());
EXPECT_FALSE(last_request()->has_user_gesture);
reset_last_request();
ScopedTestUserActivation test_user_activation;
RunFunction(function, context, std::size(argv), argv);
ASSERT_TRUE(last_request());
EXPECT_TRUE(last_request()->has_user_gesture);
reset_last_request();
}
TEST_F(APIBindingUnittest, FilteredEvents) {
const char kEvents[] =
"[{"
" 'name': 'unfilteredOne',"
" 'parameters': []"
"}, {"
" 'name': 'unfilteredTwo',"
" 'filters': [],"
" 'parameters': []"
"}, {"
" 'name': 'unfilteredThree',"
" 'options': {'supportsFilters': false},"
" 'parameters': []"
"}, {"
" 'name': 'filteredOne',"
" 'options': {'supportsFilters': true},"
" 'parameters': []"
"}, {"
" 'name': 'filteredTwo',"
" 'filters': ["
" {'name': 'url', 'type': 'array', 'items': {'type': 'any'}}"
" ],"
" 'parameters': []"
"}]";
SetEvents(kEvents);
InitializeBinding();
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
v8::Local<v8::Object> binding_object = binding()->CreateInstance(context);
const char kAddFilteredListener[] =
"(function(evt) {\n"
" evt.addListener(function() {},\n"
" {url: [{pathContains: 'simple2.html'}]});\n"
"})";
v8::Local<v8::Function> function =
FunctionFromString(context, kAddFilteredListener);
ASSERT_FALSE(function.IsEmpty());
auto check_supports_filters = [context, binding_object, function](
std::string_view name,
bool expect_supports) {
SCOPED_TRACE(name);
v8::Local<v8::Value> event =
GetPropertyFromObject(binding_object, context, name);
v8::Local<v8::Value> args[] = {event};
if (expect_supports) {
RunFunction(function, context, context->Global(), std::size(args), args);
} else {
RunFunctionAndExpectError(
function, context, context->Global(), std::size(args), args,
"Uncaught TypeError: This event does not support filters");
}
};
check_supports_filters("unfilteredOne", false);
check_supports_filters("unfilteredTwo", false);
check_supports_filters("unfilteredThree", false);
check_supports_filters("filteredOne", true);
check_supports_filters("filteredTwo", true);
}
TEST_F(APIBindingUnittest, HooksTemplateInitializer) {
SetFunctions(kFunctions);
// Register a hook for the test.oneString method.
auto hooks = std::make_unique<APIBindingHooksTestDelegate>();
auto hook = [](v8::Isolate* isolate,
v8::Local<v8::ObjectTemplate> object_template,
const APITypeReferenceMap& type_refs) {
object_template->Set(gin::StringToSymbol(isolate, "hookedProperty"),
gin::ConvertToV8(isolate, 42));
};
hooks->SetTemplateInitializer(base::BindRepeating(hook));
SetHooksDelegate(std::move(hooks));
InitializeBinding();
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
v8::Local<v8::Object> binding_object = binding()->CreateInstance(context);
// The extra property should be present on the binding object.
EXPECT_EQ("42", GetStringPropertyFromObject(binding_object, context,
"hookedProperty"));
// Sanity check: other values should still be there.
EXPECT_EQ("function",
GetStringPropertyFromObject(binding_object, context, "oneString"));
}
TEST_F(APIBindingUnittest, HooksInstanceInitializer) {
SetFunctions(kFunctions);
static constexpr char kHookedProperty[] = "hookedProperty";
// Register a hook for the test.oneString method.
auto hooks = std::make_unique<APIBindingHooksTestDelegate>();
int count = 0;
auto hook = [](int* count, v8::Local<v8::Context> context,
v8::Local<v8::Object> object) {
v8::Isolate* isolate = v8::Isolate::GetCurrent();
// Add a new property only for the first instance.
if ((*count)++ == 0) {
object
->Set(context, gin::StringToSymbol(isolate, kHookedProperty),
gin::ConvertToV8(isolate, 42))
.ToChecked();
}
};
hooks->SetInstanceInitializer(base::BindRepeating(hook, &count));
SetHooksDelegate(std::move(hooks));
InitializeBinding();
v8::HandleScope handle_scope(isolate());
// Create two instances.
v8::Local<v8::Context> context1 = MainContext();
v8::Local<v8::Object> binding_object1 = binding()->CreateInstance(context1);
v8::Local<v8::Context> context2 = AddContext();
v8::Local<v8::Object> binding_object2 = binding()->CreateInstance(context2);
// We should have run the hooks twice (once per instance).
EXPECT_EQ(2, count);
// The extra property should be present on the first binding object, but not
// the second.
EXPECT_EQ("42", GetStringPropertyFromObject(binding_object1, context1,
kHookedProperty));
EXPECT_EQ("undefined", GetStringPropertyFromObject(binding_object2, context2,
kHookedProperty));
// Sanity check: other values should still be there.
EXPECT_EQ("function", GetStringPropertyFromObject(binding_object1, context1,
"oneString"));
EXPECT_EQ("function", GetStringPropertyFromObject(binding_object2, context1,
"oneString"));
}
// Test that running hooks returning different results correctly sends requests
// or notifies of silent requests.
TEST_F(APIBindingUnittest, TestSendingRequestsAndSilentRequestsWithHooks) {
SetFunctions(
"[{"
" 'name': 'modifyArgs',"
" 'parameters': []"
"}, {"
" 'name': 'invalidInvocation',"
" 'parameters': []"
"}, {"
" 'name': 'throwException',"
" 'parameters': []"
"}, {"
" 'name': 'dontHandle',"
" 'parameters': []"
"}, {"
" 'name': 'handle',"
" 'parameters': []"
"}, {"
" 'name': 'handleAndSendRequest',"
" 'parameters': []"
"}, {"
" 'name': 'handleWithArgs',"
" 'parameters': [{"
" 'name': 'first',"
" 'type': 'string'"
" }, {"
" 'name': 'second',"
" 'type': 'integer'"
" }]"
"}]");
using RequestResult = APIBindingHooks::RequestResult;
auto basic_handler =
[](RequestResult::ResultCode code, const APISignature*,
v8::Local<v8::Context> context, v8::LocalVector<v8::Value>* arguments,
const APITypeReferenceMap& map) { return RequestResult(code); };
auto hooks = std::make_unique<APIBindingHooksTestDelegate>();
hooks->AddHandler(
"test.modifyArgs",
base::BindRepeating(basic_handler, RequestResult::ARGUMENTS_UPDATED));
hooks->AddHandler(
"test.invalidInvocation",
base::BindRepeating(basic_handler, RequestResult::INVALID_INVOCATION));
hooks->AddHandler(
"test.dontHandle",
base::BindRepeating(basic_handler, RequestResult::NOT_HANDLED));
hooks->AddHandler("test.handle",
base::BindRepeating(basic_handler, RequestResult::HANDLED));
hooks->AddHandler(
"test.throwException",
base::BindRepeating([](const APISignature*,
v8::Local<v8::Context> context,
v8::LocalVector<v8::Value>* arguments,
const APITypeReferenceMap& map) {
v8::Isolate* isolate = v8::Isolate::GetCurrent();
isolate->ThrowException(gin::StringToV8(isolate, "some error"));
return RequestResult(RequestResult::THROWN);
}));
hooks->AddHandler(
"test.handleWithArgs",
base::BindRepeating([](const APISignature*,
v8::Local<v8::Context> context,
v8::LocalVector<v8::Value>* arguments,
const APITypeReferenceMap& map) {
arguments->push_back(v8::Integer::New(v8::Isolate::GetCurrent(), 42));
return RequestResult(RequestResult::HANDLED);
}));
auto handle_and_send_request =
[](APIRequestHandler* handler, const APISignature*,
v8::Local<v8::Context> context, v8::LocalVector<v8::Value>* arguments,
const APITypeReferenceMap& map) {
handler->StartRequest(
context, "test.handleAndSendRequest", base::Value::List(),
binding::AsyncResponseType::kNone, v8::Local<v8::Function>(),
v8::Local<v8::Function>(), binding::ResultModifierFunction());
return RequestResult(RequestResult::HANDLED);
};
hooks->AddHandler(
"test.handleAndSendRequest",
base::BindRepeating(handle_and_send_request, request_handler()));
SetHooksDelegate(std::move(hooks));
auto on_silent_request = [](std::optional<std::string>* name_out,
std::optional<std::vector<std::string>>* args_out,
v8::Local<v8::Context> context,
const std::string& call_name,
const v8::LocalVector<v8::Value>& arguments) {
*name_out = call_name;
*args_out = std::vector<std::string>();
(*args_out)->reserve(arguments.size());
for (const auto& arg : arguments) {
(*args_out)->push_back(V8ToString(arg, context));
}
};
std::optional<std::string> silent_request;
std::optional<std::vector<std::string>> request_arguments;
SetOnSilentRequest(base::BindRepeating(on_silent_request, &silent_request,
&request_arguments));
InitializeBinding();
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
v8::Local<v8::Object> binding_object = binding()->CreateInstance(context);
auto call_api_method = [binding_object, context](
std::string_view name,
std::string_view string_args) {
v8::Local<v8::Function> call = FunctionFromString(
context, base::StringPrintf("(function(binding) { binding.%s(%s); })",
name.data(), string_args.data()));
v8::Local<v8::Value> args[] = {binding_object};
v8::Isolate* isolate = v8::Isolate::GetCurrent();
v8::TryCatch try_catch(isolate);
// The throwException call will throw an exception; ignore it.
std::ignore =
call->Call(context, v8::Undefined(isolate), std::size(args), args);
};
call_api_method("modifyArgs", "");
ASSERT_TRUE(last_request());
EXPECT_EQ("test.modifyArgs", last_request()->method_name);
EXPECT_FALSE(silent_request);
reset_last_request();
silent_request.reset();
request_arguments.reset();
call_api_method("invalidInvocation", "");
EXPECT_FALSE(last_request());
EXPECT_FALSE(silent_request);
reset_last_request();
silent_request.reset();
request_arguments.reset();
call_api_method("throwException", "");
EXPECT_FALSE(last_request());
EXPECT_FALSE(silent_request);
reset_last_request();
silent_request.reset();
request_arguments.reset();
call_api_method("dontHandle", "");
ASSERT_TRUE(last_request());
EXPECT_EQ("test.dontHandle", last_request()->method_name);
EXPECT_FALSE(silent_request);
reset_last_request();
silent_request.reset();
request_arguments.reset();
call_api_method("handle", "");
EXPECT_FALSE(last_request());
ASSERT_TRUE(silent_request);
EXPECT_EQ("test.handle", *silent_request);
ASSERT_TRUE(request_arguments);
EXPECT_TRUE(request_arguments->empty());
reset_last_request();
silent_request.reset();
request_arguments.reset();
call_api_method("handleAndSendRequest", "");
ASSERT_TRUE(last_request());
EXPECT_EQ("test.handleAndSendRequest", last_request()->method_name);
EXPECT_FALSE(silent_request);
reset_last_request();
silent_request.reset();
request_arguments.reset();
call_api_method("handleWithArgs", "'str'");
EXPECT_FALSE(last_request());
ASSERT_TRUE(silent_request);
ASSERT_EQ("test.handleWithArgs", *silent_request);
ASSERT_TRUE(request_arguments);
EXPECT_THAT(
*request_arguments,
testing::ElementsAre("\"str\"", "42")); // 42 was added by the handler.
reset_last_request();
silent_request.reset();
request_arguments.reset();
}
// Test native hooks that don't handle the result, but set a custom callback
// instead.
TEST_F(APIBindingUnittest, TestHooksWithCustomCallback) {
SetFunctions(kFunctions);
// Register a hook for the test.oneString method.
auto hooks = std::make_unique<APIBindingHooksTestDelegate>();
auto hook_with_custom_callback =
[](const APISignature* signature, v8::Local<v8::Context> context,
v8::LocalVector<v8::Value>* arguments,
const APITypeReferenceMap& ref_map) {
constexpr char kCustomCallback[] =
"(function() { this.calledCustomCallback = true; })";
v8::Local<v8::Function> custom_callback =
FunctionFromString(context, kCustomCallback);
APIBindingHooks::RequestResult result(
APIBindingHooks::RequestResult::NOT_HANDLED, custom_callback);
return result;
};
hooks->AddHandler("test.oneString",
base::BindRepeating(hook_with_custom_callback));
SetHooksDelegate(std::move(hooks));
InitializeBinding();
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
v8::Local<v8::Object> binding_object = binding()->CreateInstance(context);
// First try calling the oneString() method, which has a custom hook
// installed.
v8::Local<v8::Function> func =
FunctionFromString(context, "(function(obj) { obj.oneString('foo'); })");
v8::Local<v8::Value> args[] = {binding_object};
RunFunction(func, context, 1, args);
ASSERT_TRUE(last_request());
EXPECT_TRUE(last_request()->has_async_response_handler);
request_handler()->CompleteRequest(last_request()->request_id,
base::Value::List(), std::string());
EXPECT_EQ("true", GetStringPropertyFromObject(context->Global(), context,
"calledCustomCallback"));
}
// Test native hooks that don't handle the result, but add a result modifier.
TEST_F(APIBindingUnittest, TestHooksWithResultModifier) {
SetFunctions(kFunctionsWithPromiseSignatures);
bool context_allows_promises = true;
SetPromiseAvailabilityFlag(&context_allows_promises);
// Register a hook for the test.supportsPromises method with a result modifier
// that changes the result when the async response type is callback based.
auto hooks = std::make_unique<APIBindingHooksTestDelegate>();
int total_modifier_call_count = 0;
auto result_modifier = [&total_modifier_call_count](
const v8::LocalVector<v8::Value>& result_args,
v8::Local<v8::Context> context,
binding::AsyncResponseType async_type) {
total_modifier_call_count++;
if (async_type == binding::AsyncResponseType::kCallback) {
// For callback based calls change the result to a vector with
// multiple arguments by appending "bar" to the end.
v8::Isolate* isolate = v8::Isolate::GetCurrent();
v8::LocalVector<v8::Value> new_args(
isolate, {result_args[0], gin::StringToV8(isolate, "bar")});
return new_args;
}
return result_args;
};
auto hook_with_result_modifier =
[&result_modifier](const APISignature* signature,
v8::Local<v8::Context> context,
v8::LocalVector<v8::Value>* arguments,
const APITypeReferenceMap& ref_map) {
APIBindingHooks::RequestResult result(
APIBindingHooks::RequestResult::NOT_HANDLED,
v8::Local<v8::Function>(),
base::BindLambdaForTesting(result_modifier));
return result;
};
hooks->AddHandler("test.supportsPromises",
base::BindLambdaForTesting(hook_with_result_modifier));
SetHooksDelegate(std::move(hooks));
InitializeBinding();
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
v8::Local<v8::Object> binding_object = binding()->CreateInstance(context);
// A promise-based call should remain unmodified and return as normal.
{
v8::Local<v8::Function> promise_api_call = FunctionFromString(
context, "(function(api) { return api.supportsPromises(1); });");
v8::Local<v8::Value> args[] = {binding_object};
v8::Local<v8::Value> api_result =
RunFunctionOnGlobal(promise_api_call, context, std::size(args), args);
v8::Local<v8::Promise> promise;
ASSERT_TRUE(GetValueAs(api_result, &promise));
EXPECT_EQ(v8::Promise::kPending, promise->State());
ASSERT_TRUE(last_request());
request_handler()->CompleteRequest(last_request()->request_id,
ListValueFromString(R"(["foo"])"),
std::string());
EXPECT_EQ(v8::Promise::kFulfilled, promise->State());
EXPECT_EQ(R"("foo")", V8ToString(promise->Result(), context));
EXPECT_EQ(1, total_modifier_call_count);
}
// A callback-based call will be modified by the hook and return with multiple
// parameters.
{
constexpr char kFunctionCall[] =
R"((function(api) {
api.supportsPromises(2, (normalResult, addedResult) => {
this.argument1 = normalResult;
this.argument2 = addedResult;
});
}))";
v8::Local<v8::Function> callback_api_call =
FunctionFromString(context, kFunctionCall);
v8::Local<v8::Value> args[] = {binding_object};
RunFunctionOnGlobal(callback_api_call, context, std::size(args), args);
ASSERT_TRUE(last_request());
request_handler()->CompleteRequest(last_request()->request_id,
ListValueFromString(R"(["foo"])"),
std::string());
EXPECT_EQ(R"("foo")", GetStringPropertyFromObject(context->Global(),
context, "argument1"));
EXPECT_EQ(R"("bar")", GetStringPropertyFromObject(context->Global(),
context, "argument2"));
EXPECT_EQ(2, total_modifier_call_count);
}
// A call which results in an error should reject as expected and the result
// modifier should never be called.
{
v8::Local<v8::Function> promise_api_call = FunctionFromString(
context, "(function(api) { return api.supportsPromises(3) });");
v8::Local<v8::Value> args[] = {binding_object};
v8::Local<v8::Value> api_result =
RunFunctionOnGlobal(promise_api_call, context, std::size(args), args);
v8::Local<v8::Promise> promise = api_result.As<v8::Promise>();
ASSERT_FALSE(api_result.IsEmpty());
EXPECT_EQ(v8::Promise::kPending, promise->State());
ASSERT_TRUE(last_request());
request_handler()->CompleteRequest(last_request()->request_id,
base::Value::List(), "Error message");
EXPECT_EQ(v8::Promise::kRejected, promise->State());
ASSERT_TRUE(promise->Result()->IsObject());
EXPECT_EQ(R"("Error message")",
GetStringPropertyFromObject(promise->Result().As<v8::Object>(),
context, "message"));
// Since the result modifier should have never been called, the total call
// count should still be the same as in the previous test case.
EXPECT_EQ(2, total_modifier_call_count);
}
}
// Test native hooks that add a result modifier are compatible with JS hooks
// which handle the request.
TEST_F(APIBindingUnittest, TestHooksWithResultModifierAndJSHook) {
bool context_allows_promises = true;
SetPromiseAvailabilityFlag(&context_allows_promises);
// Register a JS hook for supportsPromises.
const char kRegisterHook[] = R"(
(function(hooks) {
hooks.setHandleRequest('supportsPromises', (firstArg, callback) => {
// Call the callback, appending "-foo" to the argument passed in.
callback(firstArg + '-foo');
});
}))";
InitializeJSHooks(kRegisterHook);
SetFunctions(kFunctionsWithPromiseSignatures);
// Register a native hook for test.supportsPromises with a result modifier
// that changes the result when the async response type is callback based.
auto hooks = std::make_unique<APIBindingHooksTestDelegate>();
auto result_modifier = [](const v8::LocalVector<v8::Value>& result_args,
v8::Local<v8::Context> context,
binding::AsyncResponseType async_type) {
if (async_type == binding::AsyncResponseType::kCallback) {
// For callback based calls change the result to a vector with
// multiple arguments by appending "bar" to the end.
v8::Isolate* isolate = v8::Isolate::GetCurrent();
v8::LocalVector<v8::Value> new_args(
isolate, {result_args[0], gin::StringToV8(isolate, "bar")});
return new_args;
}
return result_args;
};
auto hook_with_result_modifier =
[&result_modifier](const APISignature* signature,
v8::Local<v8::Context> context,
v8::LocalVector<v8::Value>* arguments,
const APITypeReferenceMap& ref_map) {
APIBindingHooks::RequestResult result(
APIBindingHooks::RequestResult::NOT_HANDLED,
v8::Local<v8::Function>(), base::BindOnce(result_modifier));
return result;
};
// Normally handlers are bound using base::BindRepeating, but to bind a lambda
// with a capture we have to use BindLambdaForTesting.
hooks->AddHandler("test.supportsPromises",
base::BindLambdaForTesting(hook_with_result_modifier));
SetHooksDelegate(std::move(hooks));
InitializeBinding();
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
v8::Local<v8::Object> binding_object = binding()->CreateInstance(context);
// A promise-based call should just be modified by the JS hook..
{
v8::Local<v8::Function> promise_api_call = FunctionFromString(
context, "(function(api) { return api.supportsPromises(1); });");
v8::Local<v8::Value> args[] = {binding_object};
v8::Local<v8::Value> api_result =
RunFunctionOnGlobal(promise_api_call, context, std::size(args), args);
// Since the JS callback completes the request right away, the promise
// should already be fulfilled without us needing to manually complete the
// request.
v8::Local<v8::Promise> promise;
ASSERT_TRUE(GetValueAs(api_result, &promise));
EXPECT_EQ(v8::Promise::kFulfilled, promise->State());
EXPECT_EQ(R"("1-foo")", V8ToString(promise->Result(), context));
}
// A callback-based call will be modified by the native hook to return with
// multiple parameters, as well as having the first parameter modified by the
// JS hook.
{
constexpr char kFunctionCall[] =
R"((function(api) {
api.supportsPromises(2, (normalResult, addedResult) => {
this.argument1 = normalResult;
this.argument2 = addedResult;
});
}))";
v8::Local<v8::Function> promise_api_call =
FunctionFromString(context, kFunctionCall);
v8::Local<v8::Value> args[] = {binding_object};
RunFunctionOnGlobal(promise_api_call, context, std::size(args), args);
EXPECT_EQ(R"("2-foo")", GetStringPropertyFromObject(context->Global(),
context, "argument1"));
EXPECT_EQ(R"("bar")", GetStringPropertyFromObject(context->Global(),
context, "argument2"));
}
}
TEST_F(APIBindingUnittest, AccessAPIMethodsAndEventsAfterInvalidation) {
SetEvents(R"([{"name": "onFoo"}])");
InitializeBinding();
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
v8::Local<v8::Object> binding_object = binding()->CreateInstance(context);
v8::Local<v8::Function> function = FunctionFromString(
context, "(function(obj) { obj.onFoo.addListener(function() {}); })");
binding::InvalidateContext(context);
v8::Local<v8::Value> argv[] = {binding_object};
RunFunctionAndExpectError(function, context, std::size(argv), argv,
"Uncaught Error: Extension context invalidated.");
}
TEST_F(APIBindingUnittest, CallbackSignaturesAreAdded) {
std::unique_ptr<base::AutoReset<bool>> response_validation_override =
binding::SetResponseValidationEnabledForTesting(true);
SetFunctions(kFunctionsWithCallbackSignatures);
InitializeBinding();
{
const APISignature* signature =
type_refs().GetAPIMethodSignature("test.noCallback");
ASSERT_TRUE(signature);
EXPECT_FALSE(signature->has_async_return());
EXPECT_FALSE(signature->has_async_return_signature());
}
{
const APISignature* signature =
type_refs().GetAPIMethodSignature("test.intCallback");
ASSERT_TRUE(signature);
EXPECT_TRUE(signature->has_async_return());
EXPECT_TRUE(signature->has_async_return_signature());
}
{
const APISignature* signature =
type_refs().GetAPIMethodSignature("test.noParamCallback");
ASSERT_TRUE(signature);
EXPECT_TRUE(signature->has_async_return());
EXPECT_TRUE(signature->has_async_return_signature());
}
}
TEST_F(APIBindingUnittest,
CallbackSignaturesAreNotAddedWhenValidationDisabled) {
std::unique_ptr<base::AutoReset<bool>> response_validation_override =
binding::SetResponseValidationEnabledForTesting(false);
SetFunctions(kFunctionsWithCallbackSignatures);
InitializeBinding();
EXPECT_FALSE(
type_refs().GetAPIMethodSignature("test.noCallback")->has_async_return());
EXPECT_TRUE(type_refs()
.GetAPIMethodSignature("test.intCallback")
->has_async_return());
EXPECT_FALSE(type_refs()
.GetAPIMethodSignature("test.intCallback")
->has_async_return_signature());
EXPECT_TRUE(type_refs()
.GetAPIMethodSignature("test.noParamCallback")
->has_async_return());
EXPECT_FALSE(type_refs()
.GetAPIMethodSignature("test.noParamCallback")
->has_async_return_signature());
}
// Tests promise-based APIs exposed on bindings.
TEST_F(APIBindingUnittest, PromiseBasedAPIs) {
SetFunctions(kFunctionsWithPromiseSignatures);
// Set a local boolean we can change to simulate if the context supports
// promises or not.
bool context_allows_promises = true;
SetPromiseAvailabilityFlag(&context_allows_promises);
InitializeBinding();
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
v8::Local<v8::Object> binding_object = binding()->CreateInstance(context);
// A normal call into the promised based API should return a promise. When the
// request is completed with a value, the promise will be resolved with that
// value.
{
v8::Local<v8::Function> promise_api_call = FunctionFromString(
context, "(function(api) { return api.supportsPromises(3); })");
v8::Local<v8::Value> args[] = {binding_object};
v8::Local<v8::Value> result =
RunFunctionOnGlobal(promise_api_call, context, std::size(args), args);
v8::Local<v8::Promise> promise;
ASSERT_TRUE(GetValueAs(result, &promise));
EXPECT_EQ(v8::Promise::kPending, promise->State());
ASSERT_TRUE(last_request());
request_handler()->CompleteRequest(last_request()->request_id,
ListValueFromString(R"(["foo"])"),
std::string());
EXPECT_EQ(v8::Promise::kFulfilled, promise->State());
EXPECT_EQ(R"("foo")", V8ToString(promise->Result(), context));
}
// Also test that promise-based APIs still support passing a callback.
{
constexpr char kFunctionCall[] =
R"((function(api) {
api.supportsPromises(3, (strResult) => {
this.callbackResult = strResult
});
}))";
v8::Local<v8::Function> promise_api_call =
FunctionFromString(context, kFunctionCall);
v8::Local<v8::Value> args[] = {binding_object};
RunFunctionOnGlobal(promise_api_call, context, std::size(args), args);
ASSERT_TRUE(last_request());
request_handler()->CompleteRequest(last_request()->request_id,
ListValueFromString(R"(["bar"])"),
std::string());
EXPECT_EQ(R"("bar")", GetStringPropertyFromObject(
context->Global(), context, "callbackResult"));
}
// If a request is completed with an error, the promise should be rejected.
{
v8::Local<v8::Function> promise_api_call = FunctionFromString(
context, "(function(api) { return api.supportsPromises(3) });");
v8::Local<v8::Value> args[] = {binding_object};
v8::Local<v8::Value> api_result =
RunFunctionOnGlobal(promise_api_call, context, std::size(args), args);
v8::Local<v8::Promise> promise = api_result.As<v8::Promise>();
ASSERT_FALSE(api_result.IsEmpty());
EXPECT_EQ(v8::Promise::kPending, promise->State());
ASSERT_TRUE(last_request());
request_handler()->CompleteRequest(last_request()->request_id,
base::Value::List(), "Error message");
EXPECT_EQ(v8::Promise::kRejected, promise->State());
ASSERT_TRUE(promise->Result()->IsObject());
EXPECT_EQ(R"("Error message")",
GetStringPropertyFromObject(promise->Result().As<v8::Object>(),
context, "message"));
}
// If a request is completed with a result and an error, the promise should be
// rejected and the result will not be returned. Note: ideally no APIs would
// do this but some legacy APIs do it through returning ErrorWithArguments as
// their ResponseValue. This testcase documents how this behaves with
// promises.
{
v8::Local<v8::Function> promise_api_call = FunctionFromString(
context, "(function(api) { return api.supportsPromises(3) });");
v8::Local<v8::Value> args[] = {binding_object};
v8::Local<v8::Value> api_result =
RunFunctionOnGlobal(promise_api_call, context, std::size(args), args);
v8::Local<v8::Promise> promise = api_result.As<v8::Promise>();
ASSERT_FALSE(api_result.IsEmpty());
EXPECT_EQ(v8::Promise::kPending, promise->State());
ASSERT_TRUE(last_request());
request_handler()->CompleteRequest(last_request()->request_id,
ListValueFromString(R"(["bar"])"),
"Error message");
EXPECT_EQ(v8::Promise::kRejected, promise->State());
ASSERT_TRUE(promise->Result()->IsObject());
EXPECT_EQ(R"("Error message")",
GetStringPropertyFromObject(promise->Result().As<v8::Object>(),
context, "message"));
}
// If the context doesn't support promises, there should be an error if a
// required callback isn't supplied.
context_allows_promises = false;
{
v8::Local<v8::Function> promise_api_call = FunctionFromString(
context, "(function(api) { return api.supportsPromises(3) });");
v8::Local<v8::Value> args[] = {binding_object};
auto expected_error =
"Uncaught TypeError: " +
api_errors::InvocationError("test.supportsPromises",
"integer int, function callback",
api_errors::NoMatchingSignature());
RunFunctionAndExpectError(promise_api_call, context, std::size(args), args,
expected_error);
}
// Test that required callbacks still work when the context doesn't support
// promises.
{
constexpr char kFunctionCall[] =
R"((function(api) {
api.supportsPromises(3, (strResult) => {
this.callbackResult = strResult
});
}))";
v8::Local<v8::Function> promise_api_call =
FunctionFromString(context, kFunctionCall);
v8::Local<v8::Value> args[] = {binding_object};
RunFunctionOnGlobal(promise_api_call, context, std::size(args), args);
ASSERT_TRUE(last_request());
request_handler()->CompleteRequest(last_request()->request_id,
ListValueFromString(R"(["foo"])"),
std::string());
EXPECT_EQ(R"("foo")", GetStringPropertyFromObject(
context->Global(), context, "callbackResult"));
}
// If a returns_async field is marked as optional, then a context which
// doesn't support promises should be able to leave it off of the call.
{
v8::Local<v8::Function> promise_api_call = FunctionFromString(
context, "(function(api) { return api.callbackOptional(3) });");
v8::Local<v8::Value> args[] = {binding_object};
v8::Local<v8::Value> api_result =
RunFunctionOnGlobal(promise_api_call, context, std::size(args), args);
ASSERT_TRUE(last_request());
ASSERT_TRUE(api_result->IsNullOrUndefined());
}
}
TEST_F(APIBindingUnittest, TestPromisesWithJSCustomCallback) {
// Set a local boolean we can change to simulate if the context supports
// promises or not.
bool context_allows_promises = true;
SetPromiseAvailabilityFlag(&context_allows_promises);
// Register a custom callback hook for the supportsPromises method.
const char kRegisterHook[] = R"(
(function(hooks) {
hooks.setCustomCallback('supportsPromises',
(callback, response) => {
this.response = response;
this.resolveCallback = callback;
if (response == 'resolveNow')
callback('bar');
});
}))";
InitializeJSHooks(kRegisterHook);
SetFunctions(kFunctionsWithPromiseSignatures);
InitializeBinding();
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
v8::Local<v8::Object> binding_object = binding()->CreateInstance(context);
// A normal call into the promise-based API should return a promise.
{
v8::Local<v8::Function> promise_api_call = FunctionFromString(
context, "(function(api) { return api.supportsPromises(1); });");
v8::Local<v8::Value> args[] = {binding_object};
v8::Local<v8::Value> api_result =
RunFunctionOnGlobal(promise_api_call, context, std::size(args), args);
v8::Local<v8::Promise> promise;
ASSERT_TRUE(GetValueAs(api_result, &promise));
EXPECT_EQ(v8::Promise::kPending, promise->State());
ASSERT_TRUE(last_request());
request_handler()->CompleteRequest(last_request()->request_id,
ListValueFromString(R"(["foo"])"),
std::string());
// The promise should still be unfulfilled until the callback is invoked.
EXPECT_EQ(v8::Promise::kPending, promise->State());
v8::Local<v8::Function> resolve_callback;
ASSERT_TRUE(GetPropertyFromObjectAs(context->Global(), context,
"resolveCallback", &resolve_callback));
v8::Local<v8::Value> callback_arguments[] = {
GetPropertyFromObject(context->Global(), context, "response")};
EXPECT_EQ(R"("foo")", V8ToString(callback_arguments[0], context));
RunFunctionOnGlobal(resolve_callback, context,
std::size(callback_arguments), callback_arguments);
EXPECT_EQ(v8::Promise::kFulfilled, promise->State());
EXPECT_EQ(R"("foo")", V8ToString(promise->Result(), context));
}
// Sending a response to the hook to make it resolve immediately should result
// in the promise being resolved right after CompleteRequest is called.
{
v8::Local<v8::Function> promise_api_call = FunctionFromString(
context, "(function(api) { return api.supportsPromises(2); });");
v8::Local<v8::Value> args[] = {binding_object};
v8::Local<v8::Value> api_result =
RunFunctionOnGlobal(promise_api_call, context, std::size(args), args);
v8::Local<v8::Promise> promise;
ASSERT_TRUE(GetValueAs(api_result, &promise));
EXPECT_EQ(v8::Promise::kPending, promise->State());
ASSERT_TRUE(last_request());
request_handler()->CompleteRequest(last_request()->request_id,
ListValueFromString(R"(["resolveNow"])"),
std::string());
EXPECT_EQ(v8::Promise::kFulfilled, promise->State());
EXPECT_EQ(R"("bar")", V8ToString(promise->Result(), context));
}
// Completing the request with an error should still call into the custom
// callback, which will reject the promise with the error when the callback
// passed to it is called.
{
v8::Local<v8::Function> promise_api_call = FunctionFromString(
context, "(function(api) { return api.supportsPromises(3) });");
v8::Local<v8::Value> args[] = {binding_object};
v8::Local<v8::Value> api_result =
RunFunctionOnGlobal(promise_api_call, context, std::size(args), args);
v8::Local<v8::Promise> promise = api_result.As<v8::Promise>();
ASSERT_FALSE(api_result.IsEmpty());
EXPECT_EQ(v8::Promise::kPending, promise->State());
ASSERT_TRUE(last_request());
request_handler()->CompleteRequest(last_request()->request_id,
ListValueFromString(R"(["baz"])"),
"Error message");
EXPECT_EQ(v8::Promise::kPending, promise->State());
v8::Local<v8::Value> resolve_callback =
GetPropertyFromObject(context->Global(), context, "resolveCallback");
ASSERT_TRUE(resolve_callback->IsFunction());
RunFunctionOnGlobal(resolve_callback.As<v8::Function>(), context, 0,
nullptr);
EXPECT_EQ(v8::Promise::kRejected, promise->State());
ASSERT_TRUE(promise->Result()->IsObject());
EXPECT_EQ(R"("Error message")",
GetStringPropertyFromObject(promise->Result().As<v8::Object>(),
context, "message"));
}
}
TEST_F(APIBindingUnittest, TestPromiseWithJSUpdateArgumentsPreValidate) {
bool context_allows_promises = true;
SetPromiseAvailabilityFlag(&context_allows_promises);
// Register an update arguments pre validate hook for supportsPromises.
const char kRegisterHook[] = R"(
(function(hooks) {
hooks.setUpdateArgumentsPreValidate('supportsPromises',
(...arguments) => {
this.firstArgument = arguments[0];
this.secondArgument = arguments[1];
if (arguments[0] == 'hooked')
arguments[0] = 42;
return arguments;
});
}))";
InitializeJSHooks(kRegisterHook);
SetFunctions(kFunctionsWithPromiseSignatures);
InitializeBinding();
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
v8::Local<v8::Object> binding_object = binding()->CreateInstance(context);
{
// Calling supportsPromises normally with a callback should work fine.
auto result =
ExpectPass(binding_object, "return obj.supportsPromises(5, () => {});",
"[5]", true);
ASSERT_FALSE(result.IsEmpty());
EXPECT_TRUE(result->IsUndefined());
EXPECT_TRUE(
GetPropertyFromObject(context->Global(), context, "secondArgument")
->IsFunction());
}
{
// Calling supportsPromises normally while omitting the callback should work
// fine.
auto result = ExpectPass(binding_object, "return obj.supportsPromises(5);",
"[5]", true);
EXPECT_TRUE(V8ValueIs<v8::Promise>(result));
}
{
// Calling supportsPromises with a string which we have not set up the
// custom hook for should cause an error.
ExpectFailure(binding_object, "obj.supportsPromises('foo');",
api_errors::InvocationError(
"test.supportsPromises", "integer int, function callback",
api_errors::NoMatchingSignature()));
EXPECT_EQ(R"("foo")", GetStringPropertyFromObject(
context->Global(), context, "firstArgument"));
}
{
// supportsPromises expects an int, but our custom hook should allow the
// string 'hooked' to work as well.
auto result = ExpectPass(
binding_object, "return obj.supportsPromises('hooked');", "[42]", true);
EXPECT_TRUE(V8ValueIs<v8::Promise>(result));
EXPECT_EQ(R"("hooked")", GetStringPropertyFromObject(
context->Global(), context, "firstArgument"));
}
{
// We should also be able to hit the custom hook with a callback still.
auto result = ExpectPass(binding_object,
"return obj.supportsPromises('hooked', () => {});",
"[42]", true);
ASSERT_FALSE(result.IsEmpty());
EXPECT_TRUE(result->IsUndefined());
EXPECT_EQ(R"("hooked")", GetStringPropertyFromObject(
context->Global(), context, "firstArgument"));
EXPECT_TRUE(
GetPropertyFromObject(context->Global(), context, "secondArgument")
->IsFunction());
}
}
TEST_F(APIBindingUnittest, TestPromiseWithJSUpdateArgumentsPostValidate) {
bool context_allows_promises = true;
SetPromiseAvailabilityFlag(&context_allows_promises);
// Register an update arguments post validate hook for supportsPromises.
const char kRegisterHook[] = R"(
(function(hooks) {
hooks.setUpdateArgumentsPostValidate('supportsPromises',
(...arguments) => {
this.firstArgument = arguments[0];
this.secondArgument = arguments[1];
arguments[0] = 'bar' + this.firstArgument;
return arguments;
});
}))";
InitializeJSHooks(kRegisterHook);
SetFunctions(kFunctionsWithPromiseSignatures);
InitializeBinding();
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
v8::Local<v8::Object> binding_object = binding()->CreateInstance(context);
{
// Calling the method with an invalid signature should never enter the hook.
ExpectFailure(binding_object, "return obj.supportsPromises('foo');",
api_errors::InvocationError(
"test.supportsPromises", "integer int, function callback",
api_errors::NoMatchingSignature()));
EXPECT_EQ("undefined", GetStringPropertyFromObject(
context->Global(), context, "firstArgument"));
}
{
// Calling supportsPromises normally with a callback should work fine and
// the arguments should be manipulated.
auto result =
ExpectPass(binding_object, "return obj.supportsPromises(5, () => {});",
R"(["bar5"])", true);
ASSERT_FALSE(result.IsEmpty());
EXPECT_TRUE(result->IsUndefined());
EXPECT_EQ(R"(5)", GetStringPropertyFromObject(context->Global(), context,
"firstArgument"));
EXPECT_TRUE(
GetPropertyFromObject(context->Global(), context, "secondArgument")
->IsFunction());
}
{
// Calling supportsPromises normally while omitting the callback should work
// fine, we should get a promise back and the arguments should be
// manipulated.
auto result = ExpectPass(binding_object, "return obj.supportsPromises(6);",
R"(["bar6"])", true);
EXPECT_TRUE(V8ValueIs<v8::Promise>(result));
EXPECT_EQ(R"(6)", GetStringPropertyFromObject(context->Global(), context,
"firstArgument"));
}
}
TEST_F(APIBindingUnittest, UnicodeArgumentsPassedCorrectly) {
SetFunctions(kFunctions);
InitializeBinding();
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
// This contains a non-BMP Unicode character, which should be correctly passed
// as an argument to the function, through the UTF-8 -> UTF-16 -> UTF-8 round
// trip.
constexpr char kSource[] = u8"(function(obj) { obj.oneString('🤡'); })";
constexpr char kExpectation[] = u8"🤡";
v8::Local<v8::Function> func = FunctionFromString(context, kSource);
ASSERT_FALSE(func.IsEmpty());
v8::Local<v8::Value> argv[] = {binding()->CreateInstance(context)};
RunFunction(func, context, 1, argv);
ASSERT_TRUE(last_request());
ASSERT_EQ(1u, last_request()->arguments_list.size());
base::Value str_value = last_request()->arguments_list.front().Clone();
ASSERT_TRUE(str_value.is_string());
ASSERT_EQ(kExpectation, *str_value.GetIfString());
}
} // namespace extensions