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