| // 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 |