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