blob: 24ebce13aa52302d81ba39c65efee6c545b6bca6 [file] [log] [blame]
// 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/gin_port.h"
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/macros.h"
#include "base/optional.h"
#include "base/stl_util.h"
#include "base/strings/stringprintf.h"
#include "extensions/common/api/messaging/message.h"
#include "extensions/common/api/messaging/port_id.h"
#include "extensions/renderer/bindings/api_binding_test.h"
#include "extensions/renderer/bindings/api_binding_test_util.h"
#include "extensions/renderer/bindings/api_event_handler.h"
#include "gin/data_object_builder.h"
#include "gin/handle.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "third_party/blink/public/web/web_scoped_user_gesture.h"
namespace extensions {
namespace {
const int kDefaultRoutingId = 42;
const char kDefaultPortName[] = "port name";
// Stub delegate for testing.
class TestPortDelegate : public GinPort::Delegate {
public:
TestPortDelegate() {}
~TestPortDelegate() override {}
void PostMessageToPort(v8::Local<v8::Context> context,
const PortId& port_id,
int routing_id,
std::unique_ptr<Message> message) override {
last_port_id_ = port_id;
last_message_ = std::move(message);
}
MOCK_METHOD3(ClosePort,
void(v8::Local<v8::Context> context,
const PortId&,
int routing_id));
void ResetLastMessage() {
last_port_id_.reset();
last_message_.reset();
}
const base::Optional<PortId>& last_port_id() const { return last_port_id_; }
const Message* last_message() const { return last_message_.get(); }
private:
base::Optional<PortId> last_port_id_;
std::unique_ptr<Message> last_message_;
DISALLOW_COPY_AND_ASSIGN(TestPortDelegate);
};
class GinPortTest : public APIBindingTest {
public:
GinPortTest() {}
~GinPortTest() override {}
void SetUp() override {
APIBindingTest::SetUp();
auto get_context_owner = [](v8::Local<v8::Context> context) {
return std::string();
};
event_handler_ = std::make_unique<APIEventHandler>(
base::DoNothing(), base::BindRepeating(get_context_owner), nullptr);
delegate_ = std::make_unique<testing::StrictMock<TestPortDelegate>>();
}
void TearDown() override {
APIBindingTest::TearDown();
event_handler_.reset();
}
void OnWillDisposeContext(v8::Local<v8::Context> context) override {
event_handler_->InvalidateContext(context);
binding::InvalidateContext(context);
}
gin::Handle<GinPort> CreatePort(v8::Local<v8::Context> context,
const PortId& port_id,
const char* name = kDefaultPortName) {
EXPECT_EQ(context, context->GetIsolate()->GetCurrentContext());
return gin::CreateHandle(
isolate(), new GinPort(context, port_id, kDefaultRoutingId, name,
event_handler(), delegate()));
}
APIEventHandler* event_handler() { return event_handler_.get(); }
TestPortDelegate* delegate() { return delegate_.get(); }
private:
std::unique_ptr<APIEventHandler> event_handler_;
std::unique_ptr<testing::StrictMock<TestPortDelegate>> delegate_;
DISALLOW_COPY_AND_ASSIGN(GinPortTest);
};
} // namespace
// Tests getting the port's name.
TEST_F(GinPortTest, TestGetName) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
PortId port_id(base::UnguessableToken::Create(), 0, true);
gin::Handle<GinPort> port = CreatePort(context, port_id);
v8::Local<v8::Object> port_obj = port.ToV8().As<v8::Object>();
EXPECT_EQ(R"("port name")",
GetStringPropertyFromObject(port_obj, context, "name"));
}
// Tests dispatching a message through the port to JS listeners.
TEST_F(GinPortTest, TestDispatchMessage) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
PortId port_id(base::UnguessableToken::Create(), 0, true);
gin::Handle<GinPort> port = CreatePort(context, port_id);
v8::Local<v8::Object> port_obj = port.ToV8().As<v8::Object>();
const char kTestFunction[] =
R"((function(port) {
this.onMessagePortValid = false;
this.messageValid = false;
port.onMessage.addListener((message, listenerPort) => {
this.onMessagePortValid = listenerPort === port;
let stringifiedMessage = JSON.stringify(message);
this.messageValid =
stringifiedMessage === '{"foo":42}' || stringifiedMessage;
});
}))";
v8::Local<v8::Function> test_function =
FunctionFromString(context, kTestFunction);
v8::Local<v8::Value> args[] = {port_obj};
RunFunctionOnGlobal(test_function, context, base::size(args), args);
port->DispatchOnMessage(context, Message(R"({"foo":42})", false));
EXPECT_EQ("true", GetStringPropertyFromObject(context->Global(), context,
"messageValid"));
EXPECT_EQ("true", GetStringPropertyFromObject(context->Global(), context,
"onMessagePortValid"));
}
// Tests posting a message from JS.
TEST_F(GinPortTest, TestPostMessage) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
PortId port_id(base::UnguessableToken::Create(), 0, true);
gin::Handle<GinPort> port = CreatePort(context, port_id);
v8::Local<v8::Object> port_obj = port.ToV8().As<v8::Object>();
auto test_post_message = [this, port_obj, context](
base::StringPiece function,
base::Optional<PortId> expected_port_id,
base::Optional<Message> expected_message) {
SCOPED_TRACE(function);
ASSERT_EQ(!!expected_port_id, !!expected_message)
<< "Cannot expect a port id with no message";
v8::Local<v8::Function> v8_function = FunctionFromString(context, function);
v8::Local<v8::Value> args[] = {port_obj};
if (expected_port_id) {
RunFunction(v8_function, context, base::size(args), args);
ASSERT_TRUE(delegate()->last_port_id());
EXPECT_EQ(*expected_port_id, delegate()->last_port_id());
ASSERT_TRUE(delegate()->last_message());
EXPECT_EQ(expected_message->data, delegate()->last_message()->data);
EXPECT_EQ(expected_message->user_gesture,
delegate()->last_message()->user_gesture);
} else {
RunFunctionAndExpectError(v8_function, context, base::size(args), args,
"Uncaught Error: Could not serialize message.");
EXPECT_FALSE(delegate()->last_port_id());
EXPECT_FALSE(delegate()->last_message())
<< delegate()->last_message()->data;
}
delegate()->ResetLastMessage();
};
{
// Simple message; should succeed.
const char kFunction[] =
"(function(port) { port.postMessage({data: [42]}); })";
test_post_message(kFunction, port_id, Message(R"({"data":[42]})", false));
}
{
// Simple non-object message; should succeed.
const char kFunction[] = "(function(port) { port.postMessage('hello'); })";
test_post_message(kFunction, port_id, Message(R"("hello")", false));
}
{
// Undefined string (interesting because of our comparison to the JSON
// stringify result "undefined"); should succeed.
const char kFunction[] =
"(function(port) { port.postMessage('undefined'); })";
test_post_message(kFunction, port_id, Message(R"("undefined")", false));
}
{
// We change undefined to null; see comment in gin_port.cc.
const char kFunction[] =
"(function(port) { port.postMessage(undefined); })";
test_post_message(kFunction, port_id, Message("null", false));
}
{
// Simple message with user gesture; should succeed.
const char kFunction[] =
"(function(port) { port.postMessage({data: [42]}); })";
blink::WebScopedUserGesture user_gesture(nullptr);
test_post_message(kFunction, port_id, Message(R"({"data":[42]})", true));
}
{
// Un-JSON-able object (self-referential). Should fail.
const char kFunction[] =
R"((function(port) {
let message = {foo: 42};
message.bar = message;
port.postMessage(message);
}))";
test_post_message(kFunction, base::nullopt, base::nullopt);
}
{
// Disconnect the port and send a message. Should fail.
port->DispatchOnDisconnect(context);
EXPECT_TRUE(port->is_closed_for_testing());
const char kFunction[] =
"(function(port) { port.postMessage({data: [42]}); })";
v8::Local<v8::Function> function = FunctionFromString(context, kFunction);
v8::Local<v8::Value> args[] = {port_obj};
RunFunctionAndExpectError(
function, context, base::size(args), args,
"Uncaught Error: Attempting to use a disconnected port object");
EXPECT_FALSE(delegate()->last_port_id());
EXPECT_FALSE(delegate()->last_message());
delegate()->ResetLastMessage();
}
}
// Tests that calling Disconnect() notifies any listeners of the onDisconnect
// event and then closes the port.
TEST_F(GinPortTest, TestNativeDisconnect) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
PortId port_id(base::UnguessableToken::Create(), 0, true);
gin::Handle<GinPort> port = CreatePort(context, port_id);
v8::Local<v8::Object> port_obj = port.ToV8().As<v8::Object>();
const char kTestFunction[] =
R"((function(port) {
this.onDisconnectPortValid = false;
port.onDisconnect.addListener(listenerPort => {
this.onDisconnectPortValid = listenerPort === port;
});
}))";
v8::Local<v8::Function> test_function =
FunctionFromString(context, kTestFunction);
v8::Local<v8::Value> args[] = {port_obj};
RunFunctionOnGlobal(test_function, context, base::size(args), args);
port->DispatchOnDisconnect(context);
EXPECT_EQ("true", GetStringPropertyFromObject(context->Global(), context,
"onDisconnectPortValid"));
EXPECT_TRUE(port->is_closed_for_testing());
}
// Tests calling disconnect() from JS.
TEST_F(GinPortTest, TestJSDisconnect) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
PortId port_id(base::UnguessableToken::Create(), 0, true);
gin::Handle<GinPort> port = CreatePort(context, port_id);
v8::Local<v8::Object> port_obj = port.ToV8().As<v8::Object>();
EXPECT_CALL(*delegate(), ClosePort(context, port_id, kDefaultRoutingId))
.Times(1);
const char kFunction[] = "(function(port) { port.disconnect(); })";
v8::Local<v8::Function> function = FunctionFromString(context, kFunction);
v8::Local<v8::Value> args[] = {port_obj};
RunFunction(function, context, base::size(args), args);
::testing::Mock::VerifyAndClearExpectations(delegate());
EXPECT_TRUE(port->is_closed_for_testing());
}
// Tests setting and getting the 'sender' property.
TEST_F(GinPortTest, TestSenderProperty) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
PortId port_id(base::UnguessableToken::Create(), 0, true);
gin::Handle<GinPort> port = CreatePort(context, port_id);
v8::Local<v8::Object> port_obj = port.ToV8().As<v8::Object>();
EXPECT_EQ("undefined",
GetStringPropertyFromObject(port_obj, context, "sender"));
port->SetSender(context,
gin::DataObjectBuilder(isolate()).Set("prop", 42).Build());
EXPECT_EQ(R"({"prop":42})",
GetStringPropertyFromObject(port_obj, context, "sender"));
}
TEST_F(GinPortTest, TryUsingPortAfterInvalidation) {
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> context = MainContext();
PortId port_id(base::UnguessableToken::Create(), 0, true);
gin::Handle<GinPort> port = CreatePort(context, port_id);
v8::Local<v8::Object> port_obj = port.ToV8().As<v8::Object>();
constexpr char kTrySendMessage[] =
"(function(port) { port.postMessage('hi'); })";
v8::Local<v8::Function> send_message_function =
FunctionFromString(context, kTrySendMessage);
constexpr char kTryDisconnect[] = "(function(port) { port.disconnect(); })";
v8::Local<v8::Function> disconnect_function =
FunctionFromString(context, kTryDisconnect);
constexpr char kTryGetOnMessage[] =
"(function(port) { return port.onMessage; })";
v8::Local<v8::Function> get_on_message_function =
FunctionFromString(context, kTryGetOnMessage);
constexpr char kTryGetOnDisconnect[] =
"(function(port) { return port.onDisconnect; })";
v8::Local<v8::Function> get_on_disconnect_function =
FunctionFromString(context, kTryGetOnDisconnect);
DisposeContext(context);
v8::Local<v8::Value> function_args[] = {port_obj};
for (const auto& function :
{send_message_function, disconnect_function, get_on_message_function,
get_on_disconnect_function}) {
SCOPED_TRACE(gin::V8ToString(isolate(),
function->ToString(context).ToLocalChecked()));
RunFunctionAndExpectError(function, context, base::size(function_args),
function_args,
"Uncaught Error: Extension context invalidated.");
}
}
} // namespace extensions