[go: nahoru, domu]

blob: 982c2514289012a724787990ea6ab705e15b6e07 [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_bindings_system_unittest.h"
#include "base/bind.h"
#include "base/callback_helpers.h"
#include "base/containers/contains.h"
#include "base/strings/stringprintf.h"
#include "extensions/common/mojom/event_dispatcher.mojom.h"
#include "extensions/renderer/bindings/api_binding.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_util.h"
#include "extensions/renderer/bindings/api_binding_types.h"
#include "extensions/renderer/bindings/api_bindings_system.h"
#include "extensions/renderer/bindings/api_invocation_errors.h"
#include "extensions/renderer/bindings/test_interaction_provider.h"
#include "gin/arguments.h"
#include "gin/converter.h"
#include "gin/try_catch.h"
#include "testing/gmock/include/gmock/gmock.h"
namespace extensions {
namespace {
// Fake API for testing.
const char kAlphaAPIName[] = "alpha";
const char kAlphaAPISpec[] = R"(
{
"types": [{
"id": "alpha.objRef",
"type": "object",
"properties": {
"prop1": {"type": "string"},
"prop2": {"type": "integer", "optional": true}
}
}, {
"id": "alpha.enumRef",
"type": "string",
"enum": ["cat", "dog"]
}],
"functions": [{
"name": "functionWithCallback",
"parameters": [{
"name": "str",
"type": "string"
}],
"returns_async": {
"name": "callback",
"parameters": [{"name": "strResult", "type": "string"}]
}
}, {
"name": "functionWithRefAndCallback",
"parameters": [{
"name": "ref",
"$ref": "alpha.objRef"
}],
"returns_async": {
"name": "callback",
"parameters": []
}
}, {
"name": "functionWithEnum",
"parameters": [{"name": "e", "$ref": "alpha.enumRef"}]
}],
"events": [{
"name": "alphaEvent",
"parameters": [{
"name": "eventArg",
"type": "object",
"properties": { "key": {"type": "integer"} }
}]
}, {
"name": "alphaOtherEvent"
}]
})";
// Another fake API for testing.
const char kBetaAPIName[] = "beta";
const char kBetaAPISpec[] = R"(
{
"functions": [{
"name": "simpleFunc",
"parameters": [{"name": "int", "type": "integer"}]
}]
})";
const char kGammaAPIName[] = "gamma";
const char kGammaAPISpec[] = R"(
{
"functions": [{
"name": "functionWithExternalRef",
"parameters": [{ "name": "someRef", "$ref": "alpha.objRef" }]
}]
})";
// JS strings used for registering custom callbacks for testing.
const char kCustomCallbackHook[] = R"(
(function(hooks) {
hooks.setCustomCallback(
'functionWithCallback', (originalCallback,
firstResult, secondResult) => {
this.results = [firstResult, secondResult];
originalCallback(secondResult);
});
}))";
const char kCustomCallbackThrowHook[] = R"(
(function(hooks) {
hooks.setCustomCallback(
'functionWithCallback', (name, originalCallback,
firstResult, secondResult) => {
throw new Error('Custom callback threw');
});
}))";
bool AllowAllAPIs(v8::Local<v8::Context> context, const std::string& name) {
return true;
}
bool AllowPromises(v8::Local<v8::Context> context) {
return true;
}
} // namespace
APIBindingsSystemTest::APIBindingsSystemTest() {}
APIBindingsSystemTest::~APIBindingsSystemTest() = default;
void APIBindingsSystemTest::SetUp() {
APIBindingTest::SetUp();
// Create the fake API schemas.
for (const auto& api : GetAPIs()) {
base::Value::Dict api_schema = DictValueFromString(api.spec);
api_schemas_[api.name] = std::move(api_schema);
}
binding::AddConsoleError add_console_error(base::BindRepeating(
&APIBindingsSystemTest::AddConsoleError, base::Unretained(this)));
auto get_context_owner = [](v8::Local<v8::Context>) {
return std::string("context");
};
bindings_system_ = std::make_unique<APIBindingsSystem>(
base::BindRepeating(&APIBindingsSystemTest::GetAPISchema,
base::Unretained(this)),
base::BindRepeating(&AllowAllAPIs), base::BindRepeating(&AllowPromises),
base::BindRepeating(&APIBindingsSystemTest::OnAPIRequest,
base::Unretained(this)),
std::make_unique<TestInteractionProvider>(),
base::BindRepeating(&APIBindingsSystemTest::OnEventListenersChanged,
base::Unretained(this)),
base::BindRepeating(get_context_owner), base::DoNothing(),
add_console_error,
APILastError(
base::BindRepeating(&APIBindingsSystemTest::GetLastErrorParent,
base::Unretained(this)),
add_console_error));
}
void APIBindingsSystemTest::TearDown() {
// Dispose all contexts now so that we call WillReleaseContext().
DisposeAllContexts();
bindings_system_.reset();
APIBindingTest::TearDown();
}
void APIBindingsSystemTest::OnWillDisposeContext(
v8::Local<v8::Context> context) {
bindings_system_->WillReleaseContext(context);
}
std::vector<APIBindingsSystemTest::FakeSpec> APIBindingsSystemTest::GetAPIs() {
return {
{kAlphaAPIName, kAlphaAPISpec},
{kBetaAPIName, kBetaAPISpec},
{kGammaAPIName, kGammaAPISpec},
};
}
v8::Local<v8::Object> APIBindingsSystemTest::GetLastErrorParent(
v8::Local<v8::Context> context,
v8::Local<v8::Object>* secondary_parent) {
return v8::Local<v8::Object>();
}
void APIBindingsSystemTest::AddConsoleError(v8::Local<v8::Context> context,
const std::string& error) {
console_errors_.push_back(error);
}
const base::Value::Dict& APIBindingsSystemTest::GetAPISchema(
const std::string& api_name) {
EXPECT_TRUE(base::Contains(api_schemas_, api_name));
return api_schemas_[api_name];
}
void APIBindingsSystemTest::OnAPIRequest(
std::unique_ptr<APIRequestHandler::Request> request,
v8::Local<v8::Context> context) {
ASSERT_FALSE(last_request_);
last_request_ = std::move(request);
}
void APIBindingsSystemTest::OnEventListenersChanged(
const std::string& event_name,
binding::EventListenersChanged changed,
const base::Value::Dict* filter,
bool was_manual,
v8::Local<v8::Context> context) {}
void APIBindingsSystemTest::ValidateLastRequest(
const std::string& expected_name,
const std::string& expected_arguments) {
ASSERT_TRUE(last_request());
EXPECT_EQ(expected_name, last_request()->method_name);
EXPECT_EQ(ReplaceSingleQuotes(expected_arguments),
ValueToString(last_request()->arguments_list));
}
v8::Local<v8::Value> APIBindingsSystemTest::CallFunctionOnObject(
v8::Local<v8::Context> context,
v8::Local<v8::Object> object,
const std::string& script_source) {
std::string wrapped_script_source =
base::StringPrintf("(function(obj) { %s })", script_source.c_str());
v8::Local<v8::Function> func =
FunctionFromString(context, wrapped_script_source);
// Use ADD_FAILURE() to avoid messing up the return type with ASSERT.
if (func.IsEmpty()) {
ADD_FAILURE() << script_source;
return v8::Local<v8::Value>();
}
v8::Local<v8::Value> argv[] = {object};
return RunFunction(func, context, 1, argv);
}
// Tests API object initialization, calling a method on the supplied APIs, and
// triggering the callback for the request.
TEST_F(APIBindingsSystemTest, TestInitializationAndCallbacks) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
v8::Local<v8::Object> alpha_api =
bindings_system()->CreateAPIInstance(kAlphaAPIName, context, nullptr);
ASSERT_FALSE(alpha_api.IsEmpty());
v8::Local<v8::Object> beta_api =
bindings_system()->CreateAPIInstance(kBetaAPIName, context, nullptr);
ASSERT_FALSE(beta_api.IsEmpty());
{
// Test a simple call -> response.
const char kTestCall[] = R"(
obj.functionWithCallback('foo', function() {
this.callbackArguments = Array.from(arguments);
});)";
CallFunctionOnObject(context, alpha_api, kTestCall);
ValidateLastRequest("alpha.functionWithCallback", "['foo']");
const char kResponseArgsJson[] = R"(["response"])";
bindings_system()->CompleteRequest(last_request()->request_id,
ListValueFromString(kResponseArgsJson),
std::string());
EXPECT_EQ(kResponseArgsJson,
GetStringPropertyFromObject(context->Global(), context,
"callbackArguments"));
reset_last_request();
}
{
// Test a call with references -> response.
const char kTestCall[] = R"(
obj.functionWithRefAndCallback({prop1: 'alpha', prop2: 42},
function() {
this.callbackArguments = Array.from(arguments);
});)";
CallFunctionOnObject(context, alpha_api, kTestCall);
ValidateLastRequest("alpha.functionWithRefAndCallback",
"[{'prop1':'alpha','prop2':42}]");
bindings_system()->CompleteRequest(last_request()->request_id,
base::Value::List(), std::string());
EXPECT_EQ("[]", GetStringPropertyFromObject(context->Global(), context,
"callbackArguments"));
reset_last_request();
}
{
// Test an invalid invocation -> throwing error.
const char kTestCall[] =
"(function(obj) { obj.functionWithEnum('mouse') })";
v8::Local<v8::Function> function = FunctionFromString(context, kTestCall);
v8::Local<v8::Value> args[] = {alpha_api};
RunFunctionAndExpectError(
function, context, std::size(args), args,
"Uncaught TypeError: " +
api_errors::InvocationError(
"alpha.functionWithEnum", "alpha.enumRef e",
api_errors::ArgumentError(
"e", api_errors::InvalidEnumValue({"cat", "dog"}))));
EXPECT_FALSE(last_request());
reset_last_request(); // Just to not pollute future results.
}
{
// Test an event registration -> event occurrence.
const char kTestCall[] = R"(
obj.alphaEvent.addListener(function() {
this.eventArguments = Array.from(arguments);
});)";
CallFunctionOnObject(context, alpha_api, kTestCall);
const char kResponseArgsJson[] = R"([{"key":42}])";
base::Value::List expected_args = ListValueFromString(kResponseArgsJson);
bindings_system()->FireEventInContext("alpha.alphaEvent", context,
expected_args, nullptr);
EXPECT_EQ(kResponseArgsJson,
GetStringPropertyFromObject(context->Global(), context,
"eventArguments"));
}
{
// Test a call -> response on the second API.
const char kTestCall[] = "obj.simpleFunc(2)";
CallFunctionOnObject(context, beta_api, kTestCall);
ValidateLastRequest("beta.simpleFunc", "[2]");
reset_last_request();
}
}
// Tests adding a custom hook to an API.
TEST_F(APIBindingsSystemTest, TestCustomHooks) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
bool did_call = false;
auto hook = [](bool* did_call, const APISignature* signature,
v8::Local<v8::Context> context,
std::vector<v8::Local<v8::Value>>* arguments,
const APITypeReferenceMap& type_refs) {
*did_call = true;
APIBindingHooks::RequestResult result(
APIBindingHooks::RequestResult::HANDLED);
if (arguments->size() != 2) { // ASSERT* messes with the return type.
EXPECT_EQ(2u, arguments->size());
return result;
}
std::string argument;
EXPECT_EQ("foo", gin::V8ToString(context->GetIsolate(), arguments->at(0)));
if (!arguments->at(1)->IsFunction()) {
EXPECT_TRUE(arguments->at(1)->IsFunction());
return result;
}
v8::Local<v8::String> response =
gin::StringToV8(context->GetIsolate(), "bar");
v8::Local<v8::Value> response_args[] = {response};
RunFunctionOnGlobal(arguments->at(1).As<v8::Function>(),
context, 1, response_args);
return result;
};
auto test_hooks = std::make_unique<APIBindingHooksTestDelegate>();
test_hooks->AddHandler("alpha.functionWithCallback",
base::BindRepeating(hook, &did_call));
APIBindingHooks* binding_hooks =
bindings_system()->GetHooksForAPI(kAlphaAPIName);
binding_hooks->SetDelegate(std::move(test_hooks));
v8::Local<v8::Object> alpha_api =
bindings_system()->CreateAPIInstance(kAlphaAPIName, context, nullptr);
ASSERT_FALSE(alpha_api.IsEmpty());
{
// Test a simple call -> response.
const char kTestCall[] = R"(
obj.functionWithCallback('foo', function() {
this.callbackArguments = Array.from(arguments);
});)";
CallFunctionOnObject(context, alpha_api, kTestCall);
EXPECT_TRUE(did_call);
EXPECT_EQ(R"(["bar"])",
GetStringPropertyFromObject(context->Global(), context,
"callbackArguments"));
}
}
// Tests a call with a callback into an API using a setCustomCallback hook
// works as expected.
TEST_F(APIBindingsSystemTest, TestSetCustomCallback_SuccessWithCallback) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
APIBindingHooks* hooks = nullptr;
v8::Local<v8::Object> alpha_api =
bindings_system()->CreateAPIInstance(kAlphaAPIName, context, &hooks);
ASSERT_FALSE(alpha_api.IsEmpty());
ASSERT_TRUE(hooks);
v8::Local<v8::Object> js_hooks = hooks->GetJSHookInterface(context);
v8::Local<v8::Function> function =
FunctionFromString(context, kCustomCallbackHook);
v8::Local<v8::Value> args[] = {js_hooks};
RunFunctionOnGlobal(function, context, std::size(args), args);
const char kTestCall[] = R"(
obj.functionWithCallback('foo', function() {
this.callbackArguments = Array.from(arguments);
});)";
CallFunctionOnObject(context, alpha_api, kTestCall);
ValidateLastRequest("alpha.functionWithCallback", "['foo']");
// Although this response would violate the return on the spec, since this
// method has a custom callback defined it skips response validation. We
// expect the custom callback will transform the return to the correct form
// when calling the original callback, but this is not currently enforced or
// validated.
// TODO(tjudkins): Now that we use the CustomCallbackAdaptor, we could
// potentially send the response validator to the custom callback adaptor
// and validate the result returned from the custom callback before sending
// it on to the original callback.
bindings_system()->CompleteRequest(last_request()->request_id,
ListValueFromString(R"(["alpha","beta"])"),
std::string());
EXPECT_EQ(R"(["alpha","beta"])",
GetStringPropertyFromObject(context->Global(), context, "results"));
EXPECT_EQ(R"(["beta"])",
GetStringPropertyFromObject(context->Global(), context,
"callbackArguments"));
}
// Tests a call with a promise into an API using a setCustomCallback hook works
// as expected.
TEST_F(APIBindingsSystemTest, TestSetCustomCallback_SuccessWithPromise) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
APIBindingHooks* hooks = nullptr;
v8::Local<v8::Object> alpha_api =
bindings_system()->CreateAPIInstance(kAlphaAPIName, context, &hooks);
ASSERT_FALSE(alpha_api.IsEmpty());
ASSERT_TRUE(hooks);
v8::Local<v8::Object> js_hooks = hooks->GetJSHookInterface(context);
v8::Local<v8::Function> function =
FunctionFromString(context, kCustomCallbackHook);
v8::Local<v8::Value> args[] = {js_hooks};
RunFunctionOnGlobal(function, context, std::size(args), args);
const char kTestCall[] = R"(return obj.functionWithCallback('bar');)";
v8::Local<v8::Value> result =
CallFunctionOnObject(context, alpha_api, kTestCall);
ValidateLastRequest("alpha.functionWithCallback", "['bar']");
v8::Local<v8::Promise> promise;
ASSERT_TRUE(GetValueAs(result, &promise));
EXPECT_EQ(v8::Promise::kPending, promise->State());
bindings_system()->CompleteRequest(
last_request()->request_id, ListValueFromString(R"(["gamma","delta"])"),
std::string());
EXPECT_EQ(R"(["gamma","delta"])",
GetStringPropertyFromObject(context->Global(), context, "results"));
EXPECT_EQ(v8::Promise::kFulfilled, promise->State());
EXPECT_EQ(R"("delta")", V8ToString(promise->Result(), context));
}
// Tests that an error thrown in a setCustomCallback hook while using a callback
// based call works as expected.
TEST_F(APIBindingsSystemTest, TestSetCustomCallback_ErrorWithCallback) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
APIBindingHooks* hooks = nullptr;
v8::Local<v8::Object> alpha_api =
bindings_system()->CreateAPIInstance(kAlphaAPIName, context, &hooks);
ASSERT_FALSE(alpha_api.IsEmpty());
ASSERT_TRUE(hooks);
v8::Local<v8::Object> js_hooks = hooks->GetJSHookInterface(context);
v8::Local<v8::Function> function =
FunctionFromString(context, kCustomCallbackThrowHook);
v8::Local<v8::Value> args[] = {js_hooks};
RunFunctionOnGlobal(function, context, std::size(args), args);
const char kTestCall[] = R"(
obj.functionWithCallback('baz', function() {
this.callbackCalled = true;
});)";
CallFunctionOnObject(context, alpha_api, kTestCall);
ValidateLastRequest("alpha.functionWithCallback", "['baz']");
ASSERT_TRUE(console_errors().empty());
TestJSRunner::AllowErrors allow_errors;
bindings_system()->CompleteRequest(
last_request()->request_id, ListValueFromString(R"(["alpha", "beta"])"),
std::string());
// The callback should have never been called and there should now be a
// console error logged.
EXPECT_EQ("undefined", GetStringPropertyFromObject(context->Global(), context,
"callbackCalled"));
ASSERT_EQ(1u, console_errors().size());
EXPECT_THAT(console_errors()[0],
testing::StartsWith(
"Error handling response: Error: Custom callback threw"));
}
// Tests that an error thrown in a setCustomCallback hook while using a promise
// based call works as expected.
TEST_F(APIBindingsSystemTest, TestSetCustomCallback_ErrorWithPromise) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
APIBindingHooks* hooks = nullptr;
v8::Local<v8::Object> alpha_api =
bindings_system()->CreateAPIInstance(kAlphaAPIName, context, &hooks);
ASSERT_FALSE(alpha_api.IsEmpty());
ASSERT_TRUE(hooks);
v8::Local<v8::Object> js_hooks = hooks->GetJSHookInterface(context);
v8::Local<v8::Function> function =
FunctionFromString(context, kCustomCallbackThrowHook);
v8::Local<v8::Value> args[] = {js_hooks};
RunFunctionOnGlobal(function, context, std::size(args), args);
const char kTestCall[] = R"(return obj.functionWithCallback('boz');)";
v8::Local<v8::Value> result =
CallFunctionOnObject(context, alpha_api, kTestCall);
ValidateLastRequest("alpha.functionWithCallback", "['boz']");
v8::Local<v8::Promise> promise;
ASSERT_TRUE(GetValueAs(result, &promise));
EXPECT_EQ(v8::Promise::kPending, promise->State());
ASSERT_TRUE(console_errors().empty());
TestJSRunner::AllowErrors allow_errors;
bindings_system()->CompleteRequest(
last_request()->request_id, ListValueFromString(R"(["gamma", "delta"])"),
std::string());
// The promise will remain pending and there should now be a console error
// logged.
// TODO(tjudkins): Ideally we should be rejecting the promise here instead.
EXPECT_EQ(v8::Promise::kPending, promise->State());
ASSERT_EQ(1u, console_errors().size());
EXPECT_THAT(console_errors()[0],
testing::StartsWith(
"Error handling response: Error: Custom callback threw"));
}
// Test that references to other API's types works.
TEST_F(APIBindingsSystemTest, CrossAPIReferences) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
// Instantiate gamma API. Note: It's important that we haven't instantiated
// alpha API yet, since this tests that we can lazily populate the type
// information.
v8::Local<v8::Object> gamma_api =
bindings_system()->CreateAPIInstance(kGammaAPIName, context, nullptr);
ASSERT_FALSE(gamma_api.IsEmpty());
{
// Test a simple call -> response.
const char kTestCall[] = "obj.functionWithExternalRef({prop1: 'foo'});";
CallFunctionOnObject(context, gamma_api, kTestCall);
ValidateLastRequest("gamma.functionWithExternalRef", "[{'prop1':'foo'}]");
reset_last_request();
}
}
TEST_F(APIBindingsSystemTest, TestCustomEvent) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
auto create_custom_event = [](v8::Local<v8::Context> context,
const std::string& event_name) {
v8::Isolate* isolate = context->GetIsolate();
v8::Local<v8::Object> ret = v8::Object::New(isolate);
ret->Set(context, gin::StringToSymbol(isolate, "name"),
gin::StringToSymbol(isolate, event_name))
.ToChecked();
return ret.As<v8::Value>();
};
auto test_hooks = std::make_unique<APIBindingHooksTestDelegate>();
test_hooks->SetCustomEvent(base::BindRepeating(create_custom_event));
APIBindingHooks* binding_hooks =
bindings_system()->GetHooksForAPI(kAlphaAPIName);
binding_hooks->SetDelegate(std::move(test_hooks));
v8::Local<v8::Object> api =
bindings_system()->CreateAPIInstance(kAlphaAPIName, context, nullptr);
v8::Local<v8::Object> event;
ASSERT_TRUE(GetPropertyFromObjectAs(api, context, "alphaEvent", &event));
EXPECT_EQ(R"("alpha.alphaEvent")",
GetStringPropertyFromObject(event, context, "name"));
v8::Local<v8::Value> event2 =
GetPropertyFromObject(api, context, "alphaEvent");
EXPECT_EQ(event, event2);
v8::Local<v8::Object> other_event;
ASSERT_TRUE(
GetPropertyFromObjectAs(api, context, "alphaOtherEvent", &other_event));
EXPECT_EQ(R"("alpha.alphaOtherEvent")",
GetStringPropertyFromObject(other_event, context, "name"));
EXPECT_NE(event, other_event);
}
} // namespace extensions