910e62b5创建于 1月15日历史提交
// 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) {}

size_t GetNumListeners(v8::Isolate* isolate, v8::Local<v8::Object> event) {
  EventEmitter* emitter = nullptr;
  gin::Converter<EventEmitter*>::FromV8(isolate, event, &emitter);
  CHECK(emitter);
  return emitter->GetNumListenersForTesting();
}

}  // 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::Object> on_baz_event =
      GetPropertyFromObject(binding_object, context, "onBaz").As<v8::Object>();
  v8::Local<v8::Value> args[] = {on_baz_event};
  RunFunction(add_listener, context, std::size(args), args);
  EXPECT_EQ(1u, GetNumListeners(isolate(), on_baz_event));
  RunFunctionAndExpectError(add_listener, context, std::size(args), args,
                            "Uncaught TypeError: Too many listeners.");
  EXPECT_EQ(1u, GetNumListeners(isolate(), on_baz_event));

  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