[Extensions Bindings] Add native tabs hooks

Add native hooks for the tabs API to use with sendRequest, sendMessage,
and connect. Refactor a bit to pull out common code into
messaging_util. Add tests for the new API hooks.

Currently, the TabsHooksDelegate is only used in testing, and is not
enabled (pending the rest of messaging being hooked up with native
bindings).

Bug: 653596
Change-Id: I985ac774b2ad52ec08261ff31a90cf5e97e343a5
Reviewed-on: https://chromium-review.googlesource.com/734720
Commit-Queue: Devlin <[email protected]>
Reviewed-by: Istiaque Ahmed <[email protected]>
Reviewed-by: Jeremy Roman <[email protected]>
Cr-Commit-Position: refs/heads/master@{#514566}
diff --git a/extensions/renderer/BUILD.gn b/extensions/renderer/BUILD.gn
index a8cf46d..9bb856d 100644
--- a/extensions/renderer/BUILD.gn
+++ b/extensions/renderer/BUILD.gn
@@ -335,6 +335,8 @@
     "bindings/api_binding_test_util.h",
     "native_extension_bindings_system_test_base.cc",
     "native_extension_bindings_system_test_base.h",
+    "send_message_tester.cc",
+    "send_message_tester.h",
     "string_source_map.cc",
     "string_source_map.h",
     "test_v8_extension_configuration.cc",
diff --git a/extensions/renderer/messaging_util.cc b/extensions/renderer/messaging_util.cc
index 0b374f9..44f7ae7 100644
--- a/extensions/renderer/messaging_util.cc
+++ b/extensions/renderer/messaging_util.cc
@@ -9,11 +9,17 @@
 #include "base/logging.h"
 #include "extensions/common/api/messaging/message.h"
 #include "gin/converter.h"
+#include "gin/dictionary.h"
 #include "third_party/WebKit/public/web/WebUserGestureIndicator.h"
 
 namespace extensions {
 namespace messaging_util {
 
+const char kSendMessageChannel[] = "chrome.runtime.sendMessage";
+const char kSendRequestChannel[] = "chrome.extension.sendRequest";
+
+const int kNoFrameId = -1;
+
 std::unique_ptr<Message> MessageFromV8(v8::Local<v8::Context> context,
                                        v8::Local<v8::Value> value) {
   DCHECK(!value.IsEmpty());
@@ -74,5 +80,79 @@
   return parsed_message;
 }
 
+int ExtractIntegerId(v8::Local<v8::Value> value) {
+  // Account for -0, which is a valid integer, but is stored as a number in v8.
+  DCHECK(value->IsNumber() &&
+         (value->IsInt32() || value.As<v8::Number>()->Value() == 0.0));
+  return value->Int32Value();
+}
+
+ParseOptionsResult ParseMessageOptions(v8::Local<v8::Context> context,
+                                       v8::Local<v8::Object> v8_options,
+                                       int flags,
+                                       MessageOptions* options_out,
+                                       std::string* error_out) {
+  DCHECK(!v8_options.IsEmpty());
+  DCHECK(!v8_options->IsNull());
+
+  v8::Isolate* isolate = context->GetIsolate();
+
+  MessageOptions options;
+
+  // Theoretically, our argument matching code already checked the types of
+  // the properties on v8_connect_options. However, since we don't make an
+  // independent copy, it's possible that author script has super sneaky
+  // getters/setters that change the result each time the property is
+  // queried. Make no assumptions.
+  gin::Dictionary options_dict(isolate, v8_options);
+  if ((flags & PARSE_CHANNEL_NAME) != 0) {
+    v8::Local<v8::Value> v8_channel_name;
+    if (!options_dict.Get("name", &v8_channel_name))
+      return THROWN;
+
+    if (!v8_channel_name->IsUndefined()) {
+      if (!v8_channel_name->IsString()) {
+        *error_out = "connectInfo.name must be a string.";
+        return TYPE_ERROR;
+      }
+      options.channel_name = gin::V8ToString(v8_channel_name);
+    }
+  }
+
+  if ((flags & PARSE_INCLUDE_TLS_CHANNEL_ID) != 0) {
+    v8::Local<v8::Value> v8_include_tls_channel_id;
+    if (!options_dict.Get("includeTlsChannelId", &v8_include_tls_channel_id))
+      return THROWN;
+
+    if (!v8_include_tls_channel_id->IsUndefined()) {
+      if (!v8_include_tls_channel_id->IsBoolean()) {
+        *error_out = "connectInfo.includeTlsChannelId must be a boolean.";
+        return TYPE_ERROR;
+      }
+      options.include_tls_channel_id =
+          v8_include_tls_channel_id->BooleanValue();
+    }
+  }
+
+  if ((flags & PARSE_FRAME_ID) != 0) {
+    v8::Local<v8::Value> v8_frame_id;
+    if (!options_dict.Get("frameId", &v8_frame_id))
+      return THROWN;
+
+    if (!v8_frame_id->IsUndefined()) {
+      if (!v8_frame_id->IsInt32() &&
+          (!v8_frame_id->IsNumber() ||
+           v8_frame_id.As<v8::Number>()->Value() != 0.0)) {
+        *error_out = "connectInfo.frameId must be an integer.";
+        return TYPE_ERROR;
+      }
+      options.frame_id = v8_frame_id->Int32Value();
+    }
+  }
+
+  *options_out = std::move(options);
+  return SUCCESS;
+}
+
 }  // namespace messaging_util
 }  // namespace extensions
diff --git a/extensions/renderer/messaging_util.h b/extensions/renderer/messaging_util.h
index 2fb604f..8dd891b5 100644
--- a/extensions/renderer/messaging_util.h
+++ b/extensions/renderer/messaging_util.h
@@ -6,6 +6,7 @@
 #define EXTENSIONS_RENDERER_MESSAGING_UTIL_H_
 
 #include <memory>
+#include <string>
 
 #include "v8/include/v8.h"
 
@@ -14,6 +15,12 @@
 
 namespace messaging_util {
 
+// The channel names for the sendMessage and sendRequest calls.
+extern const char kSendMessageChannel[];
+extern const char kSendRequestChannel[];
+
+extern const int kNoFrameId;
+
 // Parses the message from a v8 value, returning null on failure.
 std::unique_ptr<Message> MessageFromV8(v8::Local<v8::Context> context,
                                        v8::Local<v8::Value> value);
@@ -23,6 +30,43 @@
 v8::Local<v8::Value> MessageToV8(v8::Local<v8::Context> context,
                                  const Message& message);
 
+// Extracts an integer id from |value|, including accounting for -0 (which is a
+// valid integer, but is stored in V8 as a number). This will DCHECK that
+// |value| is either an int32 or -0.
+int ExtractIntegerId(v8::Local<v8::Value> value);
+
+// The result of the call to ParseMessageOptions().
+enum ParseOptionsResult {
+  TYPE_ERROR,  // The arguments were invalid.
+  THROWN,      // The script threw an error during parsing.
+  SUCCESS,     // Parsing succeeded.
+};
+
+// Flags for ParseMessageOptions().
+enum ParseOptionsFlags {
+  NO_FLAGS = 0,
+  PARSE_CHANNEL_NAME = 1,
+  PARSE_FRAME_ID = 1 << 1,
+  PARSE_INCLUDE_TLS_CHANNEL_ID = 1 << 2,
+};
+
+struct MessageOptions {
+  std::string channel_name;
+  int frame_id = kNoFrameId;
+  bool include_tls_channel_id = false;
+};
+
+// Parses the parameters sent to sendMessage or connect, returning the result of
+// the attempted parse. If |check_for_channel_name| is true, also checks for a
+// provided channel name (this is only true for connect() calls). Populates the
+// result in |params_out| or |error_out| (depending on the success of the
+// parse).
+ParseOptionsResult ParseMessageOptions(v8::Local<v8::Context> context,
+                                       v8::Local<v8::Object> v8_options,
+                                       int flags,
+                                       MessageOptions* options_out,
+                                       std::string* error_out);
+
 }  // namespace messaging_util
 }  // namespace extensions
 
diff --git a/extensions/renderer/one_time_message_handler_unittest.cc b/extensions/renderer/one_time_message_handler_unittest.cc
index 10a05266..64d3375 100644
--- a/extensions/renderer/one_time_message_handler_unittest.cc
+++ b/extensions/renderer/one_time_message_handler_unittest.cc
@@ -14,6 +14,7 @@
 #include "extensions/renderer/bindings/api_bindings_system.h"
 #include "extensions/renderer/bindings/api_request_handler.h"
 #include "extensions/renderer/message_target.h"
+#include "extensions/renderer/messaging_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"
@@ -24,8 +25,6 @@
 
 namespace {
 
-constexpr char kSendMessageChannel[] = "chrome.runtime.sendMessage";
-
 constexpr char kEchoArgsAndError[] =
     "(function() {\n"
     "  this.replyArgs = Array.from(arguments);\n"
@@ -95,18 +94,18 @@
   // We should open a message port, send a message, and then close it
   // immediately.
   MessageTarget target(MessageTarget::ForExtension(extension()->id()));
-  EXPECT_CALL(
-      *ipc_message_sender(),
-      SendOpenMessageChannel(script_context(), port_id, target,
-                             kSendMessageChannel, include_tls_channel_id));
+  EXPECT_CALL(*ipc_message_sender(),
+              SendOpenMessageChannel(script_context(), port_id, target,
+                                     messaging_util::kSendMessageChannel,
+                                     include_tls_channel_id));
   EXPECT_CALL(*ipc_message_sender(),
               SendPostMessageToPort(MSG_ROUTING_NONE, port_id, message));
   EXPECT_CALL(*ipc_message_sender(),
               SendCloseMessagePort(MSG_ROUTING_NONE, port_id, true));
 
-  message_handler()->SendMessage(script_context(), port_id, target,
-                                 kSendMessageChannel, include_tls_channel_id,
-                                 message, v8::Local<v8::Function>());
+  message_handler()->SendMessage(
+      script_context(), port_id, target, messaging_util::kSendMessageChannel,
+      include_tls_channel_id, message, v8::Local<v8::Function>());
   ::testing::Mock::VerifyAndClearExpectations(ipc_message_sender());
 
   EXPECT_FALSE(message_handler()->HasPort(script_context(), port_id));
@@ -132,16 +131,16 @@
   // We should open a message port and send a message, and the message port
   // should remain open (to allow for a reply).
   MessageTarget target(MessageTarget::ForExtension(extension()->id()));
-  EXPECT_CALL(
-      *ipc_message_sender(),
-      SendOpenMessageChannel(script_context(), port_id, target,
-                             kSendMessageChannel, include_tls_channel_id));
+  EXPECT_CALL(*ipc_message_sender(),
+              SendOpenMessageChannel(script_context(), port_id, target,
+                                     messaging_util::kSendMessageChannel,
+                                     include_tls_channel_id));
   EXPECT_CALL(*ipc_message_sender(),
               SendPostMessageToPort(MSG_ROUTING_NONE, port_id, message));
 
   message_handler()->SendMessage(script_context(), port_id, target,
-                                 kSendMessageChannel, include_tls_channel_id,
-                                 message, callback);
+                                 messaging_util::kSendMessageChannel,
+                                 include_tls_channel_id, message, callback);
   ::testing::Mock::VerifyAndClearExpectations(ipc_message_sender());
 
   // We should have added a pending request to the APIRequestHandler, but
@@ -178,15 +177,15 @@
       FunctionFromString(context, kEchoArgsAndError);
 
   MessageTarget target(MessageTarget::ForExtension(extension()->id()));
-  EXPECT_CALL(
-      *ipc_message_sender(),
-      SendOpenMessageChannel(script_context(), port_id, target,
-                             kSendMessageChannel, include_tls_channel_id));
+  EXPECT_CALL(*ipc_message_sender(),
+              SendOpenMessageChannel(script_context(), port_id, target,
+                                     messaging_util::kSendMessageChannel,
+                                     include_tls_channel_id));
   EXPECT_CALL(*ipc_message_sender(),
               SendPostMessageToPort(MSG_ROUTING_NONE, port_id, message));
   message_handler()->SendMessage(script_context(), port_id, target,
-                                 kSendMessageChannel, include_tls_channel_id,
-                                 message, callback);
+                                 messaging_util::kSendMessageChannel,
+                                 include_tls_channel_id, message, callback);
   ::testing::Mock::VerifyAndClearExpectations(ipc_message_sender());
 
   EXPECT_EQ("undefined", GetGlobalProperty(context, "replyArgs"));
diff --git a/extensions/renderer/runtime_hooks_delegate.cc b/extensions/renderer/runtime_hooks_delegate.cc
index 998c0021..139afc2 100644
--- a/extensions/renderer/runtime_hooks_delegate.cc
+++ b/extensions/renderer/runtime_hooks_delegate.cc
@@ -18,7 +18,6 @@
 #include "extensions/renderer/script_context.h"
 #include "extensions/renderer/script_context_set.h"
 #include "gin/converter.h"
-#include "gin/dictionary.h"
 
 namespace extensions {
 
@@ -51,68 +50,6 @@
   return true;
 }
 
-// The result of trying to parse options passed to a messaging API.
-enum ParseOptionsResult {
-  TYPE_ERROR,  // Invalid values were passed.
-  THROWN,      // An error was thrown while parsing.
-  SUCCESS,     // Parsing succeeded.
-};
-
-struct MessageOptions {
-  std::string channel_name;
-  bool include_tls_channel_id = false;
-};
-
-// Parses the parameters sent to sendMessage or connect, returning the result of
-// the attempted parse. If |check_for_channel_name| is true, also checks for a
-// provided channel name (this is only true for connect() calls). Populates the
-// result in |options_out| or |error_out| (depending on the success of the
-// parse).
-ParseOptionsResult ParseMessageOptions(v8::Local<v8::Context> context,
-                                       v8::Local<v8::Object> v8_options,
-                                       bool check_for_channel_name,
-                                       MessageOptions* options_out,
-                                       std::string* error_out) {
-  DCHECK(!v8_options.IsEmpty());
-  DCHECK(!v8_options->IsNull());
-
-  v8::Isolate* isolate = context->GetIsolate();
-
-  MessageOptions options;
-
-  // Theoretically, our argument matching code already checked the types of
-  // the properties on v8_connect_options. However, since we don't make an
-  // independent copy, it's possible that author script has super sneaky
-  // getters/setters that change the result each time the property is
-  // queried. Make no assumptions.
-  v8::Local<v8::Value> v8_channel_name;
-  v8::Local<v8::Value> v8_include_tls_channel_id;
-  gin::Dictionary options_dict(isolate, v8_options);
-  if (!options_dict.Get("includeTlsChannelId", &v8_include_tls_channel_id) ||
-      (check_for_channel_name && !options_dict.Get("name", &v8_channel_name))) {
-    return THROWN;
-  }
-
-  if (check_for_channel_name && !v8_channel_name->IsUndefined()) {
-    if (!v8_channel_name->IsString()) {
-      *error_out = "connectInfo.name must be a string.";
-      return TYPE_ERROR;
-    }
-    options.channel_name = gin::V8ToString(v8_channel_name);
-  }
-
-  if (!v8_include_tls_channel_id->IsUndefined()) {
-    if (!v8_include_tls_channel_id->IsBoolean()) {
-      *error_out = "connectInfo.includeTlsChannelId must be a boolean.";
-      return TYPE_ERROR;
-    }
-    options.include_tls_channel_id = v8_include_tls_channel_id->BooleanValue();
-  }
-
-  *options_out = std::move(options);
-  return SUCCESS;
-}
-
 // Massages the sendMessage() arguments into the expected schema. These
 // arguments are ambiguous (could match multiple signatures), so we can't just
 // rely on the normal signature parsing. Sets |arguments| to the result if
@@ -200,8 +137,6 @@
 constexpr char kSendMessage[] = "runtime.sendMessage";
 constexpr char kSendNativeMessage[] = "runtime.sendNativeMessage";
 
-constexpr char kSendMessageChannel[] = "chrome.runtime.sendMessage";
-
 }  // namespace
 
 RuntimeHooksDelegate::RuntimeHooksDelegate(
@@ -312,20 +247,22 @@
   }
 
   v8::Local<v8::Context> v8_context = script_context->v8_context();
-  MessageOptions options;
+  messaging_util::MessageOptions options;
   if (!arguments[2]->IsNull()) {
     std::string error;
-    ParseOptionsResult parse_result = ParseMessageOptions(
-        v8_context, arguments[2].As<v8::Object>(), false, &options, &error);
+    messaging_util::ParseOptionsResult parse_result =
+        messaging_util::ParseMessageOptions(
+            v8_context, arguments[2].As<v8::Object>(),
+            messaging_util::PARSE_INCLUDE_TLS_CHANNEL_ID, &options, &error);
     switch (parse_result) {
-      case TYPE_ERROR: {
+      case messaging_util::TYPE_ERROR: {
         RequestResult result(RequestResult::INVALID_INVOCATION);
         result.error = std::move(error);
         return result;
       }
-      case THROWN:
+      case messaging_util::THROWN:
         return RequestResult(RequestResult::THROWN);
-      case SUCCESS:
+      case messaging_util::SUCCESS:
         break;
     }
   }
@@ -345,8 +282,8 @@
 
   messaging_service_->SendOneTimeMessage(
       script_context, MessageTarget::ForExtension(target_id),
-      kSendMessageChannel, options.include_tls_channel_id, *message,
-      response_callback);
+      messaging_util::kSendMessageChannel, options.include_tls_channel_id,
+      *message, response_callback);
 
   return RequestResult(RequestResult::HANDLED);
 }
@@ -393,21 +330,24 @@
     return result;
   }
 
-  MessageOptions options;
+  messaging_util::MessageOptions options;
   if (!arguments[1]->IsNull()) {
     std::string error;
-    ParseOptionsResult parse_result = ParseMessageOptions(
-        script_context->v8_context(), arguments[1].As<v8::Object>(), true,
-        &options, &error);
+    messaging_util::ParseOptionsResult parse_result =
+        messaging_util::ParseMessageOptions(
+            script_context->v8_context(), arguments[1].As<v8::Object>(),
+            messaging_util::PARSE_INCLUDE_TLS_CHANNEL_ID |
+                messaging_util::PARSE_CHANNEL_NAME,
+            &options, &error);
     switch (parse_result) {
-      case TYPE_ERROR: {
+      case messaging_util::TYPE_ERROR: {
         RequestResult result(RequestResult::INVALID_INVOCATION);
         result.error = std::move(error);
         return result;
       }
-      case THROWN:
+      case messaging_util::THROWN:
         return RequestResult(RequestResult::THROWN);
-      case SUCCESS:
+      case messaging_util::SUCCESS:
         break;
     }
   }
diff --git a/extensions/renderer/runtime_hooks_delegate_unittest.cc b/extensions/renderer/runtime_hooks_delegate_unittest.cc
index 9d76397..659e8da 100644
--- a/extensions/renderer/runtime_hooks_delegate_unittest.cc
+++ b/extensions/renderer/runtime_hooks_delegate_unittest.cc
@@ -19,6 +19,7 @@
 #include "extensions/renderer/native_renderer_messaging_service.h"
 #include "extensions/renderer/script_context.h"
 #include "extensions/renderer/script_context_set.h"
+#include "extensions/renderer/send_message_tester.h"
 
 namespace extensions {
 namespace {
@@ -187,45 +188,22 @@
     EXPECT_TRUE(connect_native->IsUndefined());
   }
 
-  int next_context_port_id = 0;
-  auto run_connect = [this, context, &next_context_port_id](
-                         const std::string& args,
-                         const std::string& expected_channel,
-                         const std::string& expected_target_id,
-                         bool expected_include_tls_channel_id) {
-    SCOPED_TRACE(base::StringPrintf("Args: `%s`", args.c_str()));
-    constexpr char kAddPortTemplate[] =
-        "(function() { return chrome.runtime.connect(%s); })";
-    PortId expected_port_id(script_context()->context_id(),
-                            next_context_port_id++, true);
-    MessageTarget expected_target(
-        MessageTarget::ForExtension(expected_target_id));
-    EXPECT_CALL(*ipc_message_sender(),
-                SendOpenMessageChannel(script_context(), expected_port_id,
-                                       expected_target, expected_channel,
-                                       expected_include_tls_channel_id));
-    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());
-    v8::Local<v8::Object> port_obj = port.As<v8::Object>();
-    EXPECT_EQ(base::StringPrintf(R"("%s")", expected_channel.c_str()),
-              GetStringPropertyFromObject(port_obj, context, "name"));
-  };
-
-  run_connect("", "", extension()->id(), false);
-  run_connect("{name: 'channel'}", "channel", extension()->id(), false);
-  run_connect("{includeTlsChannelId: true}", "", extension()->id(), true);
-  run_connect("{includeTlsChannelId: true, name: 'channel'}", "channel",
-              extension()->id(), true);
+  SendMessageTester tester(ipc_message_sender(), script_context(), 0,
+                           "runtime");
+  MessageTarget self_target = MessageTarget::ForExtension(extension()->id());
+  tester.TestConnect("", "", self_target, false);
+  tester.TestConnect("{name: 'channel'}", "channel", self_target, false);
+  tester.TestConnect("{includeTlsChannelId: true}", "", self_target, true);
+  tester.TestConnect("{includeTlsChannelId: true, name: 'channel'}", "channel",
+                     self_target, true);
 
   std::string other_id = crx_file::id_util::GenerateId("other");
-  run_connect(base::StringPrintf("'%s'", other_id.c_str()), "", other_id,
-              false);
-  run_connect(base::StringPrintf("'%s', {name: 'channel'}", other_id.c_str()),
-              "channel", other_id, false);
+  MessageTarget other_target = MessageTarget::ForExtension(other_id);
+  tester.TestConnect(base::StringPrintf("'%s'", other_id.c_str()), "",
+                     other_target, false);
+  tester.TestConnect(
+      base::StringPrintf("'%s', {name: 'channel'}", other_id.c_str()),
+      "channel", other_target, false);
 }
 
 // Tests the end-to-end (renderer) flow for a call to runtime.sendMessage
@@ -235,12 +213,6 @@
   v8::HandleScope handle_scope(isolate());
   v8::Local<v8::Context> context = MainContext();
 
-  // Whether we expect the port to be open or closed at the end of the call.
-  enum PortStatus {
-    CLOSED,
-    OPEN,
-  };
-
   {
     // Sanity check: sendNativeMessage is unavailable (missing permission).
     v8::Local<v8::Value> send_native_message =
@@ -249,63 +221,38 @@
     EXPECT_TRUE(send_native_message->IsUndefined());
   }
 
-  int next_context_port_id = 0;
-  auto send_message = [this, context, &next_context_port_id](
-                          const std::string& args,
-                          const std::string& expected_message,
-                          const std::string& expected_target_id,
-                          bool expected_include_tls_channel_id,
-                          PortStatus expected_port_status) {
-    SCOPED_TRACE(base::StringPrintf("Args: `%s`", args.c_str()));
-    constexpr char kSendMessageTemplate[] =
-        "(function() { chrome.runtime.sendMessage(%s); })";
+  SendMessageTester tester(ipc_message_sender(), script_context(), 0,
+                           "runtime");
 
-    PortId expected_port_id(script_context()->context_id(),
-                            next_context_port_id++, true);
-    constexpr char kExpectedChannel[] = "chrome.runtime.sendMessage";
-    MessageTarget expected_target(
-        MessageTarget::ForExtension(expected_target_id));
-    EXPECT_CALL(*ipc_message_sender(),
-                SendOpenMessageChannel(script_context(), expected_port_id,
-                                       expected_target, kExpectedChannel,
-                                       expected_include_tls_channel_id));
-    Message message(expected_message, false);
-    EXPECT_CALL(
-        *ipc_message_sender(),
-        SendPostMessageToPort(MSG_ROUTING_NONE, expected_port_id, message));
-    if (expected_port_status == CLOSED) {
-      EXPECT_CALL(
-          *ipc_message_sender(),
-          SendCloseMessagePort(MSG_ROUTING_NONE, expected_port_id, true));
-    }
-    v8::Local<v8::Function> send_message = FunctionFromString(
-        context, base::StringPrintf(kSendMessageTemplate, args.c_str()));
-    RunFunction(send_message, context, 0, nullptr);
-    ::testing::Mock::VerifyAndClearExpectations(ipc_message_sender());
-  };
-
-  send_message("''", R"("")", extension()->id(), false, CLOSED);
+  MessageTarget self_target = MessageTarget::ForExtension(extension()->id());
+  tester.TestSendMessage("''", R"("")", self_target, false,
+                         SendMessageTester::CLOSED);
 
   constexpr char kStandardMessage[] = R"({"data":"hello"})";
-  send_message("{data: 'hello'}", kStandardMessage, extension()->id(), false,
-               CLOSED);
-  send_message("{data: 'hello'}, function() {}", kStandardMessage,
-               extension()->id(), false, OPEN);
-  send_message("{data: 'hello'}, {includeTlsChannelId: true}", kStandardMessage,
-               extension()->id(), true, CLOSED);
-  send_message("{data: 'hello'}, {includeTlsChannelId: true}, function() {}",
-               kStandardMessage, extension()->id(), true, OPEN);
+  tester.TestSendMessage("{data: 'hello'}", kStandardMessage, self_target,
+                         false, SendMessageTester::CLOSED);
+  tester.TestSendMessage("{data: 'hello'}, function() {}", kStandardMessage,
+                         self_target, false, SendMessageTester::OPEN);
+  tester.TestSendMessage("{data: 'hello'}, {includeTlsChannelId: true}",
+                         kStandardMessage, self_target, true,
+                         SendMessageTester::CLOSED);
+  tester.TestSendMessage(
+      "{data: 'hello'}, {includeTlsChannelId: true}, function() {}",
+      kStandardMessage, self_target, true, 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);
 
-  send_message(base::StringPrintf("'%s', {data: 'hello'}", other_id),
-               kStandardMessage, other_id_str, false, CLOSED);
-  send_message(
+  tester.TestSendMessage(base::StringPrintf("'%s', {data: 'hello'}", other_id),
+                         kStandardMessage, other_target, false,
+                         SendMessageTester::CLOSED);
+  tester.TestSendMessage(
       base::StringPrintf("'%s', {data: 'hello'}, function() {}", other_id),
-      kStandardMessage, other_id_str, false, OPEN);
-  send_message(base::StringPrintf("'%s', 'string message'", other_id),
-               R"("string message")", other_id_str, false, CLOSED);
+      kStandardMessage, other_target, false, SendMessageTester::OPEN);
+  tester.TestSendMessage(base::StringPrintf("'%s', 'string message'", other_id),
+                         R"("string message")", other_target, false,
+                         SendMessageTester::CLOSED);
 
   // 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
@@ -318,13 +265,15 @@
   // 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.
-  send_message(
+  tester.TestSendMessage(
       base::StringPrintf("'%s', {includeTlsChannelId: true}", other_id),
-      R"({"includeTlsChannelId":true})", other_id_str, false, CLOSED);
-  send_message(
+      R"({"includeTlsChannelId":true})", other_target, false,
+      SendMessageTester::CLOSED);
+  tester.TestSendMessage(
       base::StringPrintf("'%s', {includeTlsChannelId: true}, function() {}",
                          other_id),
-      R"({"includeTlsChannelId":true})", other_id_str, false, OPEN);
+      R"({"includeTlsChannelId":true})", other_target, false,
+      SendMessageTester::OPEN);
 }
 
 // Test that some incorrect invocations of sendMessage() throw errors.
diff --git a/extensions/renderer/send_message_tester.cc b/extensions/renderer/send_message_tester.cc
new file mode 100644
index 0000000..f6c5b521
--- /dev/null
+++ b/extensions/renderer/send_message_tester.cc
@@ -0,0 +1,123 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "extensions/renderer/send_message_tester.h"
+
+#include "base/strings/stringprintf.h"
+#include "extensions/renderer/bindings/api_binding_test_util.h"
+#include "extensions/renderer/messaging_util.h"
+#include "extensions/renderer/native_extension_bindings_system_test_base.h"
+#include "extensions/renderer/script_context.h"
+#include "ipc/ipc_message.h"
+#include "v8/include/v8.h"
+
+namespace extensions {
+
+SendMessageTester::SendMessageTester(TestIPCMessageSender* ipc_sender,
+                                     ScriptContext* script_context,
+                                     int next_port_id,
+                                     const std::string& api_namespace)
+    : ipc_sender_(ipc_sender),
+      script_context_(script_context),
+      next_port_id_(next_port_id),
+      api_namespace_(api_namespace) {}
+
+SendMessageTester::~SendMessageTester() {}
+
+void SendMessageTester::TestSendMessage(const std::string& args,
+                                        const std::string& expected_message,
+                                        const MessageTarget& expected_target,
+                                        bool expected_include_tls_channel_id,
+                                        PortStatus expected_port_status) {
+  SCOPED_TRACE(base::StringPrintf("Send Message Args: `%s`", args.c_str()));
+
+  TestSendMessageOrRequest(args, expected_message, expected_target,
+                           expected_include_tls_channel_id,
+                           expected_port_status, SEND_MESSAGE);
+}
+
+void SendMessageTester::TestSendRequest(const std::string& args,
+                                        const std::string& expected_message,
+                                        const MessageTarget& expected_target,
+                                        PortStatus expected_port_status) {
+  SCOPED_TRACE(base::StringPrintf("Send Request Args: `%s`", args.c_str()));
+
+  TestSendMessageOrRequest(args, expected_message, expected_target, false,
+                           expected_port_status, SEND_REQUEST);
+}
+
+void SendMessageTester::TestConnect(const std::string& args,
+                                    const std::string& expected_channel,
+                                    const MessageTarget& expected_target,
+                                    bool expected_include_tls_channel_id) {
+  SCOPED_TRACE(base::StringPrintf("Connect Args: `%s`", args.c_str()));
+
+  v8::Local<v8::Context> v8_context = script_context_->v8_context();
+
+  constexpr char kAddPortTemplate[] =
+      "(function() { return chrome.%s.connect(%s); })";
+  PortId expected_port_id(script_context_->context_id(), next_port_id_++, true);
+  EXPECT_CALL(*ipc_sender_,
+              SendOpenMessageChannel(script_context_, expected_port_id,
+                                     expected_target, expected_channel,
+                                     expected_include_tls_channel_id));
+  v8::Local<v8::Function> add_port = FunctionFromString(
+      v8_context, base::StringPrintf(kAddPortTemplate, api_namespace_.c_str(),
+                                     args.c_str()));
+  v8::Local<v8::Value> port = RunFunction(add_port, v8_context, 0, nullptr);
+  ::testing::Mock::VerifyAndClearExpectations(ipc_sender_);
+  ASSERT_FALSE(port.IsEmpty());
+  ASSERT_TRUE(port->IsObject());
+  v8::Local<v8::Object> port_obj = port.As<v8::Object>();
+  EXPECT_EQ(base::StringPrintf(R"("%s")", expected_channel.c_str()),
+            GetStringPropertyFromObject(port_obj, v8_context, "name"));
+}
+
+void SendMessageTester::TestSendMessageOrRequest(
+    const std::string& args,
+    const std::string& expected_message,
+    const MessageTarget& expected_target,
+    bool expected_include_tls_channel_id,
+    PortStatus expected_port_status,
+    Method method) {
+  constexpr char kSendMessageTemplate[] = "(function() { chrome.%s.%s(%s); })";
+
+  std::string expected_channel;
+  const char* method_name = nullptr;
+  switch (method) {
+    case SEND_MESSAGE:
+      method_name = "sendMessage";
+      expected_channel = messaging_util::kSendMessageChannel;
+      break;
+    case SEND_REQUEST:
+      method_name = "sendRequest";
+      expected_channel = messaging_util::kSendRequestChannel;
+      break;
+  }
+
+  PortId expected_port_id(script_context_->context_id(), next_port_id_++, true);
+
+  EXPECT_CALL(*ipc_sender_,
+              SendOpenMessageChannel(script_context_, expected_port_id,
+                                     expected_target, expected_channel,
+                                     expected_include_tls_channel_id));
+  Message message(expected_message, false);
+  EXPECT_CALL(*ipc_sender_, SendPostMessageToPort(MSG_ROUTING_NONE,
+                                                  expected_port_id, message));
+
+  if (expected_port_status == CLOSED) {
+    EXPECT_CALL(*ipc_sender_,
+                SendCloseMessagePort(MSG_ROUTING_NONE, expected_port_id, true));
+  }
+
+  v8::Local<v8::Context> v8_context = script_context_->v8_context();
+  v8::Local<v8::Function> send_message = FunctionFromString(
+      v8_context,
+      base::StringPrintf(kSendMessageTemplate, api_namespace_.c_str(),
+                         method_name, args.c_str()));
+  RunFunction(send_message, v8_context, 0, nullptr);
+  ::testing::Mock::VerifyAndClearExpectations(ipc_sender_);
+}
+
+}  // namespace extensions
diff --git a/extensions/renderer/send_message_tester.h b/extensions/renderer/send_message_tester.h
new file mode 100644
index 0000000..fbeebbab
--- /dev/null
+++ b/extensions/renderer/send_message_tester.h
@@ -0,0 +1,77 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef EXTENSIONS_RENDERER_SEND_MESSAGE_TESTER_H_
+#define EXTENSIONS_RENDERER_SEND_MESSAGE_TESTER_H_
+
+#include <string>
+
+#include "base/macros.h"
+
+namespace extensions {
+class ScriptContext;
+class TestIPCMessageSender;
+struct MessageTarget;
+
+// A helper class for testing the sendMessage, sendRequest, and connect API
+// calls, since these are used across three different API namespaces
+// (chrome.runtime, chrome.tabs, and chrome.extension).
+class SendMessageTester {
+ public:
+  SendMessageTester(TestIPCMessageSender* ipc_sender,
+                    ScriptContext* script_context,
+                    int next_port_id,
+                    const std::string& api_namespace);
+  ~SendMessageTester();
+
+  // Whether we expect the port to be open or closed at the end of the call.
+  enum PortStatus {
+    CLOSED,
+    OPEN,
+  };
+
+  // Tests the sendMessage API with the specified expectations.
+  void TestSendMessage(const std::string& args,
+                       const std::string& expected_message,
+                       const MessageTarget& expected_target,
+                       bool expected_include_tls_channel_id,
+                       PortStatus expected_port_status);
+
+  // Tests the sendRequest API with the specified expectations.
+  void TestSendRequest(const std::string& args,
+                       const std::string& expected_message,
+                       const MessageTarget& expected_target,
+                       PortStatus expected_port_status);
+
+  // Tests the connect API with the specified expectaions.
+  void TestConnect(const std::string& args,
+                   const std::string& expected_channel,
+                   const MessageTarget& expected_target,
+                   bool expected_include_tls_channel_id);
+
+ private:
+  enum Method {
+    SEND_REQUEST,
+    SEND_MESSAGE,
+  };
+
+  // Common handler for testing sendMessage and sendRequest.
+  void TestSendMessageOrRequest(const std::string& args,
+                                const std::string& expected_message,
+                                const MessageTarget& expected_target,
+                                bool expected_include_tls_channel_id,
+                                PortStatus expected_port_status,
+                                Method method);
+
+  TestIPCMessageSender* ipc_sender_;
+  ScriptContext* script_context_;
+  int next_port_id_;
+  std::string api_namespace_;
+
+  DISALLOW_COPY_AND_ASSIGN(SendMessageTester);
+};
+
+}  // namespace extensions
+
+#endif  // EXTENSIONS_RENDERER_SEND_MESSAGE_TESTER_H_