910e62b5创建于 1月15日历史提交
// Copyright 2017 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/api/runtime_hooks_delegate.h"

#include <memory>
#include <string_view>

#include "base/memory/raw_ptr.h"
#include "base/strings/stringprintf.h"
#include "base/values.h"
#include "components/crx_file/id_util.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_builder.h"
#include "extensions/common/mojom/context_type.mojom.h"
#include "extensions/common/mojom/frame.mojom.h"
#include "extensions/common/mojom/message_port.mojom-shared.h"
#include "extensions/renderer/api/messaging/message_target.h"
#include "extensions/renderer/api/messaging/native_renderer_messaging_service.h"
#include "extensions/renderer/api/messaging/send_message_tester.h"
#include "extensions/renderer/bindings/api_binding_test_util.h"
#include "extensions/renderer/native_extension_bindings_system.h"
#include "extensions/renderer/native_extension_bindings_system_test_base.h"
#include "extensions/renderer/script_context.h"
#include "extensions/renderer/script_context_set.h"

namespace extensions {
namespace {

void CallAPIAndExpectError(v8::Local<v8::Context> context,
                           std::string_view method_name,
                           std::string_view args) {
  SCOPED_TRACE(base::StringPrintf("Args: `%s`", args.data()));
  constexpr char kTemplate[] = "(function() { chrome.runtime.%s(%s); })";

  v8::Isolate* isolate = v8::Isolate::GetCurrent();

  // Just verify some error was thrown. Expecting the exact error message
  // tends to rely too much on our argument spec code, which is tested
  // separately.
  v8::Local<v8::Function> function = FunctionFromString(
      context, base::StringPrintf(kTemplate, method_name.data(), args.data()));
  v8::TryCatch try_catch(isolate);
  v8::MaybeLocal<v8::Value> result =
      function->Call(context, v8::Undefined(isolate), 0, nullptr);
  EXPECT_TRUE(result.IsEmpty());
  EXPECT_TRUE(try_catch.HasCaught());
}

}  // namespace

class RuntimeHooksDelegateTest : public NativeExtensionBindingsSystemUnittest {
 public:
  RuntimeHooksDelegateTest() {}

  RuntimeHooksDelegateTest(const RuntimeHooksDelegateTest&) = delete;
  RuntimeHooksDelegateTest& operator=(const RuntimeHooksDelegateTest&) = delete;

  ~RuntimeHooksDelegateTest() override {}

  // NativeExtensionBindingsSystemUnittest:
  void SetUp() override {
    NativeExtensionBindingsSystemUnittest::SetUp();
    messaging_service_ =
        std::make_unique<NativeRendererMessagingService>(bindings_system());

    bindings_system()->api_system()->RegisterHooksDelegate(
        "runtime",
        std::make_unique<RuntimeHooksDelegate>(messaging_service_.get()));

    extension_ = BuildExtension();
    RegisterExtension(extension_);

    v8::HandleScope handle_scope(isolate());
    v8::Local<v8::Context> context = MainContext();

    script_context_ = CreateScriptContext(
        context, extension_.get(), mojom::ContextType::kPrivilegedExtension);
    script_context_->set_url(extension_->url());
    bindings_system()->UpdateBindingsForContext(script_context_);
  }
  void TearDown() override {
    script_context_ = nullptr;
    extension_ = nullptr;
    messaging_service_.reset();
    NativeExtensionBindingsSystemUnittest::TearDown();
  }
  bool UseStrictIPCMessageSender() override { return true; }

  virtual scoped_refptr<const Extension> BuildExtension() {
    // TODO(https://crbug.com/40804030): Update this to use MV3.
    // SendMessageTester needs to be updated since runtime.sendMessage() now
    // returns a promise.
    return ExtensionBuilder("foo").SetManifestVersion(2).Build();
  }

  NativeRendererMessagingService* messaging_service() {
    return messaging_service_.get();
  }
  ScriptContext* script_context() { return script_context_; }
  const Extension* extension() { return extension_.get(); }

 private:
  std::unique_ptr<NativeRendererMessagingService> messaging_service_;

  raw_ptr<ScriptContext> script_context_ = nullptr;
  scoped_refptr<const Extension> extension_;
};

TEST_F(RuntimeHooksDelegateTest, RuntimeId) {
  v8::HandleScope handle_scope(isolate());
  v8::Local<v8::Context> context = MainContext();

  {
    scoped_refptr<const Extension> connectable_extension =
        ExtensionBuilder("connectable")
            .SetManifestPath("externally_connectable.matches",
                             base::Value::List().Append("*://example.com/*"))
            .Build();
    RegisterExtension(connectable_extension);
  }

  auto get_id = [](v8::Local<v8::Context> context) {
    v8::Local<v8::Function> get_id = FunctionFromString(
        context, "(function() { return chrome.runtime.id; })");
    return RunFunction(get_id, context, 0, nullptr);
  };

  {
    v8::Local<v8::Value> id = get_id(context);
    EXPECT_EQ(extension()->id(), gin::V8ToString(isolate(), id));
  }

  {
    // In order for chrome.runtime to be available to web pages, we need to have
    // an associated connectable extension, so pretend to be example.com.
    v8::Local<v8::Context> web_context = AddContext();
    ScriptContext* script_context =
        CreateScriptContext(web_context, nullptr, mojom::ContextType::kWebPage);
    script_context->set_url(GURL("http://example.com"));
    bindings_system()->UpdateBindingsForContext(script_context);
    v8::Local<v8::Value> id = get_id(web_context);
    EXPECT_TRUE(id->IsUndefined());
  }
}

TEST_F(RuntimeHooksDelegateTest, GetManifest) {
  v8::HandleScope handle_scope(isolate());
  v8::Local<v8::Context> context = MainContext();

  v8::Local<v8::Function> get_manifest = FunctionFromString(
      context, "(function() { return chrome.runtime.getManifest(); })");
  v8::Local<v8::Value> manifest =
      RunFunction(get_manifest, context, 0, nullptr);
  ASSERT_FALSE(manifest.IsEmpty());
  ASSERT_TRUE(manifest->IsObject());
  EXPECT_EQ(ValueToString(*extension()->manifest()->value()),
            V8ToString(manifest, context));
}

TEST_F(RuntimeHooksDelegateTest, GetVersion) {
  v8::HandleScope handle_scope(isolate());
  v8::Local<v8::Context> context = MainContext();

  v8::Local<v8::Function> get_version = FunctionFromString(
      context, "(function() { return chrome.runtime.getVersion(); })");
  v8::Local<v8::Value> version =
      RunFunction(get_version, context, 0, nullptr);
  ASSERT_FALSE(version.IsEmpty());
  ASSERT_TRUE(version->IsString());
  EXPECT_EQ(extension()->VersionString(),
            V8ToBaseValue(version, context)->GetString());
}

TEST_F(RuntimeHooksDelegateTest, GetURL) {
  v8::HandleScope handle_scope(isolate());
  v8::Local<v8::Context> context = MainContext();

  auto get_url = [this, context](const char* args, const GURL& expected_url) {
    SCOPED_TRACE(base::StringPrintf("Args: `%s`", args));
    constexpr char kGetUrlTemplate[] =
        "(function() { return chrome.runtime.getURL(%s); })";
    v8::Local<v8::Function> get_url =
        FunctionFromString(context, base::StringPrintf(kGetUrlTemplate, args));
    v8::Local<v8::Value> url = RunFunction(get_url, context, 0, nullptr);
    ASSERT_FALSE(url.IsEmpty());
    ASSERT_TRUE(url->IsString());
    EXPECT_EQ(expected_url.spec(), gin::V8ToString(isolate(), url));
  };

  get_url("''", extension()->url());
  get_url("'foo'", extension()->GetResourceURL("foo"));
  get_url("'/foo'", extension()->GetResourceURL("foo"));
  get_url("'https://www.google.com'",
          GURL(extension()->url().spec() + "https://www.google.com"));
}

TEST_F(RuntimeHooksDelegateTest, Connect) {
  v8::HandleScope handle_scope(isolate());
  v8::Local<v8::Context> context = MainContext();

  {
    // Sanity check: connectNative is unavailable (missing permission).
    v8::Local<v8::Value> connect_native =
        V8ValueFromScriptSource(context, "chrome.runtime.connectNative");
    ASSERT_FALSE(connect_native.IsEmpty());
    EXPECT_TRUE(connect_native->IsUndefined());
  }

  SendMessageTester tester(ipc_message_sender(), script_context(), 0,
                           "runtime");
  MessageTarget self_target = MessageTarget::ForExtension(extension()->id());
  tester.TestConnect("", "", self_target);
  tester.TestConnect("{name: 'channel'}", "channel", self_target);
  tester.TestConnect("{includeTlsChannelId: true}", "", self_target);
  tester.TestConnect("{includeTlsChannelId: true, name: 'channel'}", "channel",
                     self_target);

  std::string other_id = crx_file::id_util::GenerateId("other");
  MessageTarget other_target = MessageTarget::ForExtension(other_id);
  tester.TestConnect(base::StringPrintf("'%s'", other_id.c_str()), "",
                     other_target);
  tester.TestConnect(
      base::StringPrintf("'%s', {name: 'channel'}", other_id.c_str()),
      "channel", other_target);
}

// Tests the end-to-end (renderer) flow for a call to runtime.sendMessage
// from the call in JS to the expected IPCs. The intricacies of sendMessage
// are also tested more in the renderer messaging service unittests.
TEST_F(RuntimeHooksDelegateTest, SendMessage) {
  v8::HandleScope handle_scope(isolate());
  v8::Local<v8::Context> context = MainContext();

  {
    // Sanity check: sendNativeMessage is unavailable (missing permission).
    v8::Local<v8::Value> send_native_message =
        V8ValueFromScriptSource(context, "chrome.runtime.sendNativeMessage");
    ASSERT_FALSE(send_native_message.IsEmpty());
    EXPECT_TRUE(send_native_message->IsUndefined());
  }

  SendMessageTester tester(ipc_message_sender(), script_context(), 0,
                           "runtime");

  MessageTarget self_target = MessageTarget::ForExtension(extension()->id());
  tester.TestSendMessage("''", R"("")", self_target, SendMessageTester::CLOSED);

  constexpr char kStandardMessage[] = R"({"data":"hello"})";
  tester.TestSendMessage("{data: 'hello'}", kStandardMessage, self_target,
                         SendMessageTester::CLOSED);
  tester.TestSendMessage("{data: 'hello'}, function() {}", kStandardMessage,
                         self_target, SendMessageTester::OPEN);
  tester.TestSendMessage("{data: 'hello'}, {includeTlsChannelId: true}",
                         kStandardMessage, self_target,
                         SendMessageTester::CLOSED);
  tester.TestSendMessage(
      "{data: 'hello'}, {includeTlsChannelId: true}, function() {}",
      kStandardMessage, self_target, SendMessageTester::OPEN);

  std::string other_id_str = crx_file::id_util::GenerateId("other");
  const char* other_id = other_id_str.c_str();  // For easy StringPrintf()ing.
  MessageTarget other_target = MessageTarget::ForExtension(other_id_str);

  tester.TestSendMessage(base::StringPrintf("'%s', {data: 'hello'}", other_id),
                         kStandardMessage, other_target,
                         SendMessageTester::CLOSED);
  tester.TestSendMessage(
      base::StringPrintf("'%s', {data: 'hello'}, function() {}", other_id),
      kStandardMessage, other_target, SendMessageTester::OPEN);
  tester.TestSendMessage(base::StringPrintf("'%s', 'string message'", other_id),
                         R"("string message")", other_target,
                         SendMessageTester::CLOSED);

  // The sender could omit the ID by passing null or undefined explicitly.
  // Regression tests for https://crbug.com/828664.
  tester.TestSendMessage("null, {data: 'hello'}, function() {}",
                         kStandardMessage, self_target,
                         SendMessageTester::OPEN);
  tester.TestSendMessage("null, 'test', function() {}", R"("test")",
                         self_target, SendMessageTester::OPEN);
  tester.TestSendMessage("null, 'test'", R"("test")", self_target,
                         SendMessageTester::CLOSED);
  tester.TestSendMessage("undefined, 'test', function() {}", R"("test")",
                         self_target, SendMessageTester::OPEN);

  // Funny case. The only required argument is `message`, which can be any type.
  // This means that if an extension provides a <string, object> pair for the
  // first three arguments, it could apply to either the target id and the
  // message or to the message and the connect options.
  // In this case, we *always* pick the arguments as target id and message,
  // because they were the first options (connectOptions was added later), and
  // because connectOptions is pretty rarely used.
  // TODO(devlin): This is the determination JS has always made, but we could be
  // a bit more intelligent about it. We could examine the string to see if it's
  // a valid extension id as well as looking at the properties on the object.
  // But probably not worth it at this time.
  tester.TestSendMessage(
      base::StringPrintf("'%s', {includeTlsChannelId: true}", other_id),
      R"({"includeTlsChannelId":true})", other_target,
      SendMessageTester::CLOSED);
  tester.TestSendMessage(
      base::StringPrintf("'%s', {includeTlsChannelId: true}, function() {}",
                         other_id),
      R"({"includeTlsChannelId":true})", other_target, SendMessageTester::OPEN);
}

// Test that some incorrect invocations of sendMessage() throw errors.
TEST_F(RuntimeHooksDelegateTest, SendMessageErrors) {
  v8::HandleScope handle_scope(isolate());
  v8::Local<v8::Context> context = MainContext();

  auto send_message = [context](std::string_view args) {
    CallAPIAndExpectError(context, "sendMessage", args);
  };

  send_message("{data: 'hi'}, {unknownProp: true}");
  send_message("'some id', 'some message', 'some other string'");
  send_message("'some id', 'some message', {}, {}");
}

TEST_F(RuntimeHooksDelegateTest, ConnectWithTrickyOptions) {
  v8::HandleScope handle_scope(isolate());
  v8::Local<v8::Context> context = MainContext();

  SendMessageTester tester(ipc_message_sender(), script_context(), 0,
                           "runtime");

  MessageTarget self_target = MessageTarget::ForExtension(extension()->id());
  {
    // Even though we parse the message options separately, we do a conversion
    // of the object passed into the API. This means that something subtle like
    // this, which throws on the second access of a property, shouldn't trip us
    // up.
    constexpr char kTrickyConnectOptions[] =
        R"({
             get name() {
               if (this.checkedOnce)
                 throw new Error('tricked!');
               this.checkedOnce = true;
               return 'foo';
             }
           })";
    tester.TestConnect(kTrickyConnectOptions, "foo", self_target);
  }
  {
    // A different form of trickiness: the options object doesn't have the
    // name key (which is acceptable, since its optional), but
    // any attempt to access the key on an object without a value for it results
    // in an error. Our argument parsing code should protect us from this.
    constexpr const char kMessWithObjectPrototype[] =
        R"((function() {
             Object.defineProperty(
                 Object.prototype, 'name',
                 { get() { throw new Error('tricked!'); } });
           }))";
    v8::Local<v8::Function> mess_with_proto =
        FunctionFromString(context, kMessWithObjectPrototype);
    RunFunction(mess_with_proto, context, 0, nullptr);
    tester.TestConnect("{}", "", self_target);
  }
}

class RuntimeHooksDelegateNativeMessagingTest
    : public RuntimeHooksDelegateTest {
 public:
  RuntimeHooksDelegateNativeMessagingTest() {}
  ~RuntimeHooksDelegateNativeMessagingTest() override {}

  scoped_refptr<const Extension> BuildExtension() override {
    return ExtensionBuilder("foo").AddAPIPermission("nativeMessaging").Build();
  }
};

TEST_F(RuntimeHooksDelegateNativeMessagingTest, ConnectNative) {
  v8::HandleScope handle_scope(isolate());
  v8::Local<v8::Context> context = MainContext();

  int next_context_port_id = 0;
  auto run_connect_native = [this, context, &next_context_port_id](
                                const std::string& args,
                                const std::string& expected_app_name) {
    // connectNative() doesn't name channels.
    const std::string kEmptyExpectedChannel;

    SCOPED_TRACE(base::StringPrintf("Args: '%s'", args.c_str()));
    constexpr char kAddPortTemplate[] =
        "(function() { return chrome.runtime.connectNative(%s); })";
    PortId expected_port_id(script_context()->context_id(),
                            next_context_port_id++, true,
                            mojom::SerializationFormat::kJson);
    MessageTarget expected_target(
        MessageTarget::ForNativeApp(expected_app_name));
    EXPECT_CALL(
        *ipc_message_sender(),
        SendOpenMessageChannel(script_context(), expected_port_id,
                               expected_target, mojom::ChannelType::kNative,
                               kEmptyExpectedChannel, testing::_, testing::_));

    v8::Local<v8::Function> add_port = FunctionFromString(
        context, base::StringPrintf(kAddPortTemplate, args.c_str()));
    v8::Local<v8::Value> port = RunFunction(add_port, context, 0, nullptr);
    ::testing::Mock::VerifyAndClearExpectations(ipc_message_sender());
    ASSERT_FALSE(port.IsEmpty());
    ASSERT_TRUE(port->IsObject());
  };

  run_connect_native("'native_app'", "native_app");
  run_connect_native("'some_other_native_app'", "some_other_native_app");

  auto connect_native_error = [context](std::string_view args) {
    CallAPIAndExpectError(context, "connectNative", args);
  };
  connect_native_error("'native_app', {name: 'name'}");
}

TEST_F(RuntimeHooksDelegateNativeMessagingTest, SendNativeMessage) {
  v8::HandleScope handle_scope(isolate());
  v8::Local<v8::Context> context = MainContext();

  SendMessageTester tester(ipc_message_sender(), script_context(), 0,
                           "runtime");

  tester.TestSendNativeMessage("'native_app', {hi:'bye'}", R"({"hi":"bye"})",
                               "native_app");
  tester.TestSendNativeMessage(
      "'another_native_app', {alpha: 2}, function() {}", R"({"alpha":2})",
      "another_native_app");

  auto send_native_message_error = [context](std::string_view args) {
    CallAPIAndExpectError(context, "sendNativeMessage", args);
  };

  send_native_message_error("{data: 'hi'}, function() {}");
  send_native_message_error(
      "'native_app', 'some message', {includeTlsChannelId: true}");
}

class RuntimeHooksDelegateMV3Test : public RuntimeHooksDelegateTest {
 public:
  RuntimeHooksDelegateMV3Test() = default;
  ~RuntimeHooksDelegateMV3Test() override = default;

  scoped_refptr<const Extension> BuildExtension() override {
    return ExtensionBuilder("foo")
        .SetManifestKey("manifest_version", 3)
        .Build();
  }
};

TEST_F(RuntimeHooksDelegateMV3Test, SendMessageUsingPromise) {
  v8::HandleScope handle_scope(isolate());

  SendMessageTester tester(ipc_message_sender(), script_context(), 0,
                           "runtime");

  // The port remains open here after the call because in MV3 we return a
  // promise if the callback parameter is omitted, so we can't use the presence/
  // lack of the callback to determine if the caller is/isn't going to handle
  // the response.
  MessageTarget self_target = MessageTarget::ForExtension(extension()->id());
  tester.TestSendMessage("''", R"("")", self_target, SendMessageTester::OPEN);

  constexpr char kStandardMessage[] = R"({"data":"hello"})";
  {
    // Calling sendMessage with a callback should result in no value returned.
    v8::Local<v8::Value> result = tester.TestSendMessage(
        "{data: 'hello'}, function() {}", kStandardMessage, self_target,
        SendMessageTester::OPEN);
    EXPECT_TRUE(result->IsUndefined());
  }

  {
    // Calling sendMessage without the callback should result in a promise
    // returned.
    v8::Local<v8::Value> result =
        tester.TestSendMessage("{data: 'hello'}", kStandardMessage, self_target,
                               SendMessageTester::OPEN);
    v8::Local<v8::Promise> promise;
    ASSERT_TRUE(GetValueAs(result, &promise));
    EXPECT_EQ(v8::Promise::kPending, promise->State());
  }
}

// Test that the asynchronous return from runtime.requestUpdateCheck differs in
// structure for callback based calls vs promise based calls.
// Note: This doesn't exercise the actual implementation of the API, just that
// the bindings modify the structure of the result.
TEST_F(RuntimeHooksDelegateMV3Test, RequestUpdateCheck) {
  v8::HandleScope handle_scope(isolate());
  v8::Local<v8::Context> context = MainContext();
  constexpr char kUpdateAvailableAPIResponse[] =
      R"([{"status": "update_available", "version": "2.0"}])";

  // Calling requestUpdateCheck without a callback should return a promise that
  // gets fulfilled with an object with the results as properties on it.
  {
    v8::Local<v8::Function> func = FunctionFromString(
        context,
        "(function() { return chrome.runtime.requestUpdateCheck(); })");
    v8::Local<v8::Value> result = RunFunction(func, context, 0, nullptr);
    v8::Local<v8::Promise> promise;
    ASSERT_TRUE(GetValueAs(result, &promise));
    EXPECT_EQ(v8::Promise::kPending, promise->State());

    bindings_system()->HandleResponse(
        last_params().request_id,
        /*success=*/true, ListValueFromString(kUpdateAvailableAPIResponse),
        /*error=*/std::string());

    EXPECT_EQ(v8::Promise::kFulfilled, promise->State());
    // Note that the object here differs slightly from the response, in that it
    // is not array wrapped and any keys become alphabetized.
    EXPECT_EQ(R"({"status":"update_available","version":"2.0"})",
              V8ToString(promise->Result(), context));
  }

  // Calling requestUpdateCheck with a callback should end up with the callback
  // being called with multiple parameters rather than a single object and the
  // version info as a parameter on a details object.
  {
    constexpr char kFunctionCall[] =
        R"((function(api) {
             chrome.runtime.requestUpdateCheck((status, details) => {
               this.argument1 = status;
               this.argument2 = details;
             });
           }))";
    v8::Local<v8::Function> func = FunctionFromString(context, kFunctionCall);
    RunFunctionOnGlobal(func, context, 0, nullptr);

    bindings_system()->HandleResponse(
        last_params().request_id,
        /*success=*/true, ListValueFromString(kUpdateAvailableAPIResponse),
        /*error=*/std::string());

    EXPECT_EQ(
        R"("update_available")",
        GetStringPropertyFromObject(context->Global(), context, "argument1"));
    EXPECT_EQ(
        R"({"version":"2.0"})",
        GetStringPropertyFromObject(context->Global(), context, "argument2"));
  }

  constexpr char kNoUpdateAPIResponse[] =
      R"([{"status": "no_update", "version": ""}])";

  // If version is specified as an empty string, it will still be sent along to
  // the callback wrapped in an object.
  {
    constexpr char kFunctionCall[] =
        R"((function(api) {
             chrome.runtime.requestUpdateCheck((status, details) => {
               this.argument1 = status;
               this.argument2 = details;
             });
           }))";
    v8::Local<v8::Function> func = FunctionFromString(context, kFunctionCall);
    RunFunctionOnGlobal(func, context, 0, nullptr);

    bindings_system()->HandleResponse(last_params().request_id,
                                      /*success=*/true,
                                      ListValueFromString(kNoUpdateAPIResponse),
                                      /*error=*/std::string());

    EXPECT_EQ(R"("no_update")", GetStringPropertyFromObject(
                                    context->Global(), context, "argument1"));
    EXPECT_EQ(
        R"({"version":""})",
        GetStringPropertyFromObject(context->Global(), context, "argument2"));
  }
}

class RuntimeHooksDelegateNativeMessagingMV3Test
    : public RuntimeHooksDelegateTest {
 public:
  RuntimeHooksDelegateNativeMessagingMV3Test() = default;
  ~RuntimeHooksDelegateNativeMessagingMV3Test() override = default;

  scoped_refptr<const Extension> BuildExtension() override {
    return ExtensionBuilder("foo")
        .SetManifestKey("manifest_version", 3)
        .AddAPIPermission("nativeMessaging")
        .Build();
  }
};

TEST_F(RuntimeHooksDelegateNativeMessagingMV3Test, SendNativeMessage) {
  v8::HandleScope handle_scope(isolate());
  v8::Local<v8::Context> context = MainContext();

  SendMessageTester tester(ipc_message_sender(), script_context(), 0,
                           "runtime");

  {
    // Calling sendNativeMessage without the callback should result in a promise
    // returned.
    v8::Local<v8::Value> result = tester.TestSendNativeMessage(
        "'native_app', {hi:'bye'}", R"({"hi":"bye"})", "native_app");
    v8::Local<v8::Promise> promise;
    ASSERT_TRUE(GetValueAs(result, &promise));
    EXPECT_EQ(v8::Promise::kPending, promise->State());
  }

  {
    // Calling sendNativeMessage with a callback should result in no value
    // returned.
    v8::Local<v8::Value> result = tester.TestSendNativeMessage(
        "'another_native_app', {alpha: 2}, function() {}", R"({"alpha":2})",
        "another_native_app");
    EXPECT_TRUE(result->IsUndefined());
  }

  auto send_native_message_error = [context](std::string_view args) {
    CallAPIAndExpectError(context, "sendNativeMessage", args);
  };

  // Invoking the API with incorrect parameters should emit errors.
  send_native_message_error("{data: 'hi'}, function() {}");
  send_native_message_error(
      "'native_app', 'some message', {includeTlsChannelId: true}");
}

}  // namespace extensions