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