/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #include #include #include #include #include #include #include #include #include #include #include #include "FollyDynamicMatchers.h" #include "InspectorMocks.h" #include "UniquePtrFactory.h" using namespace ::testing; using namespace std::literals::chrono_literals; using namespace std::literals::string_literals; using folly::dynamic, folly::parseJson, folly::toJson, folly::format, folly::sformat; namespace facebook::react::jsinspector_modern { namespace { template class InspectorPackagerConnectionTestBase : public testing::Test { protected: InspectorPackagerConnectionTestBase() : packagerConnection_(InspectorPackagerConnection{ "ws://mock-host:12345", "my-app", packagerConnectionDelegates_.make_unique(asyncExecutor_)}) { ON_CALL(*packagerConnectionDelegate(), connectWebSocket(_, _)) .WillByDefault(webSockets_.lazily_make_unique< const std::string&, std::weak_ptr>()); } void TearDown() override { // Forcibly clean up all pages currently registered with the inspector in // order to isolate state between tests. NOTE: Using TearDown instead of a // destructor so that we can use FAIL() etc. std::vector pagesToRemove; auto pages = getInspectorInstance().getPages(); int liveConnectionCount = 0; for (size_t i = 0; i != localConnections_.objectsVended(); ++i) { if (localConnections_[i]) { liveConnectionCount++; // localConnections_[i] is a strict mock and will complain when we // removePage if the call is unexpected. EXPECT_CALL(*localConnections_[i], disconnect()); } } for (auto& page : pages) { getInspectorInstance().removePage(page.id); } if (!pages.empty() && liveConnectionCount) { if (!::testing::Test::HasFailure()) { FAIL() << "Test case ended with " << liveConnectionCount << " open connection(s) and " << pages.size() << " registered page(s). You must manually call removePage for each page."; } } ::testing::Test::TearDown(); } MockInspectorPackagerConnectionDelegate* packagerConnectionDelegate() { // We only create one PackagerConnectionDelegate per test. EXPECT_EQ(packagerConnectionDelegates_.objectsVended(), 1); return packagerConnectionDelegates_[0]; } Executor asyncExecutor_; UniquePtrFactory packagerConnectionDelegates_; /** * webSockets_ will hold the WebSocket instance(s) owned by * packagerConnection_ while also allowing us to access them during * the test. We can send messages *to* packagerConnection_ by * calling webSockets_[i]->getDelegate().didReceiveMessage(...). Messages * *from* packagerConnection_ will be found as calls to * webSockets_[i]->send, which is a mock method installed by gmock. * These are strict mocks, so method calls will fail if they are not * expected with a corresponding call to EXPECT_CALL(...) - for example * if unexpected WebSocket messages are sent. */ UniquePtrFactory> webSockets_; /** * localConnections_ will hold the LocalConnection instances owned * by packagerConnection_ while also allowing us to access them * during the test. * These are strict mocks, so method calls will fail if they are not * expected with a corresponding call to EXPECT_CALL(...). */ UniquePtrFactory> localConnections_; std::optional packagerConnection_; }; using InspectorPackagerConnectionTest = InspectorPackagerConnectionTestBase; /** * Fixture class for tests that run on a ManualExecutor. Work scheduled * on the executor is *not* run automatically; it must be manually advanced * in the body of the test. */ class InspectorPackagerConnectionTestAsync : public InspectorPackagerConnectionTestBase { public: virtual void TearDown() override { // Assert there are no pending tasks on the ManualExecutor. auto tasksCleared = asyncExecutor_.clear(); EXPECT_EQ(tasksCleared, 0) << "There were still pending tasks on asyncExecutor_ at the end of the test. Use advance() or run() as needed."; InspectorPackagerConnectionTestBase::TearDown(); } }; } // namespace TEST_F(InspectorPackagerConnectionTest, TestConnectThenDestroy) { packagerConnection_->connect(); // The connection should be established immediately. ASSERT_TRUE(webSockets_[0]); EXPECT_EQ(webSockets_[0]->url, "ws://mock-host:12345"); EXPECT_TRUE(packagerConnection_->isConnected()); // Destroying packagerConnection_ should close the underlying WebSocket (by // destroying it). packagerConnection_.reset(); EXPECT_FALSE(webSockets_[0]); } TEST_F(InspectorPackagerConnectionTest, TestConnectMultipleTimes) { packagerConnection_->connect(); packagerConnection_->connect(); // The WebSocket gets recreated and the connection is in a valid state. EXPECT_FALSE(webSockets_[0]); ASSERT_TRUE(webSockets_[1]); EXPECT_EQ(webSockets_[1]->url, "ws://mock-host:12345"); EXPECT_TRUE(packagerConnection_->isConnected()); // Destroying packagerConnection_ should close the underlying WebSocket (by // destroying it). packagerConnection_.reset(); EXPECT_FALSE(webSockets_[1]); } TEST_F(InspectorPackagerConnectionTest, TestCloseQuietly) { packagerConnection_->connect(); ASSERT_TRUE(webSockets_[0]); EXPECT_TRUE(packagerConnection_->isConnected()); packagerConnection_->closeQuietly(); EXPECT_FALSE(packagerConnection_->isConnected()); EXPECT_FALSE(webSockets_[0]); // Calling closeQuietly again has no effect. packagerConnection_->closeQuietly(); EXPECT_FALSE(packagerConnection_->isConnected()); EXPECT_FALSE(webSockets_[0]); // Connecting again is a noop (except for logging an error). packagerConnection_->connect(); EXPECT_FALSE(packagerConnection_->isConnected()); EXPECT_FALSE(webSockets_[0]); } TEST_F(InspectorPackagerConnectionTest, TestGetPages) { // Configure gmock to expect calls in a specific order. InSequence mockCallsMustBeInSequence; packagerConnection_->connect(); // The list of pages is empty at first. EXPECT_CALL(*webSockets_[0], send(JsonEq(R"({ "event": "getPages", "payload": [] })"))) .RetiresOnSaturation(); webSockets_[0]->getDelegate().didReceiveMessage(R"({ "event": "getPages" })"); auto pageId = getInspectorInstance().addPage( "mock-title", "mock-vm", localConnections_ .lazily_make_unique>(), {.nativePageReloads = true}); // getPages now reports the page we registered. EXPECT_CALL( *webSockets_[0], send(JsonParsed(AllOf( AtJsonPtr("/event", Eq("getPages")), AtJsonPtr( "/payload", ElementsAreArray({AllOf( AtJsonPtr("/app", Eq("my-app")), AtJsonPtr("/title", Eq("mock-title [C++ connection]")), AtJsonPtr("/vm", Eq("mock-vm")), AtJsonPtr("/id", Eq(std::to_string(pageId))), AtJsonPtr("/capabilities/nativePageReloads", Eq(true)), AtJsonPtr( "/capabilities/nativeSourceCodeFetching", Eq(false)))})))))) .RetiresOnSaturation(); webSockets_[0]->getDelegate().didReceiveMessage(R"({ "event": "getPages" })"); getInspectorInstance().removePage(pageId); // getPages is back to reporting no pages. EXPECT_CALL( *webSockets_[0], send(JsonEq( R"({ "event": "getPages", "payload": [] })"))) .RetiresOnSaturation(); webSockets_[0]->getDelegate().didReceiveMessage(R"({ "event": "getPages" })"); } TEST_F(InspectorPackagerConnectionTest, TestSendReceiveEvents) { // Configure gmock to expect calls in a specific order. InSequence mockCallsMustBeInSequence; packagerConnection_->connect(); auto pageId = getInspectorInstance().addPage( "mock-title", "mock-vm", localConnections_ .lazily_make_unique>()); // Connect to the page. webSockets_[0]->getDelegate().didReceiveMessage(sformat( R"({{ "event": "connect", "payload": {{ "pageId": {0} }} }})", toJson(std::to_string(pageId)))); ASSERT_TRUE(localConnections_[0]); // Send an event from the mocked backend (local) to the frontend (remote) // and observe it being sent via the socket. EXPECT_CALL( *webSockets_[0], send(JsonParsed(AllOf( AtJsonPtr("/event", Eq("wrappedEvent")), AtJsonPtr("/payload/pageId", Eq(std::to_string(pageId))), AtJsonPtr( "/payload/wrappedEvent", JsonEq( R"({ "method": "FakeDomain.eventTriggered", "params": ["arg1", "arg2"] })")))))) .RetiresOnSaturation(); localConnections_[0]->getRemoteConnection().onMessage(R"({ "method": "FakeDomain.eventTriggered", "params": ["arg1", "arg2"] })"); // Send an event from the frontend (remote) to the backend (local) and // observe it being received by localConnection. EXPECT_CALL( *localConnections_[0], sendMessage(JsonParsed(AllOf( AtJsonPtr("/method", Eq("FakeDomain.fakeMethod")), AtJsonPtr("/id", Eq(1234)), AtJsonPtr("/params", ElementsAre("arg1", "arg2")))))) .RetiresOnSaturation(); webSockets_[0]->getDelegate().didReceiveMessage(sformat( R"({{ "event": "wrappedEvent", "payload": {{ "pageId": {0}, "wrappedEvent": {1} }} }})", toJson(std::to_string(pageId)), toJson(R"({ "method": "FakeDomain.fakeMethod", "id": 1234, "params": ["arg1", "arg2"] })"))); // Send a 'disconnect' event from the mocked backend (local) to the frontend // (remote) and observe it being sent via the socket. EXPECT_CALL( *webSockets_[0], send(JsonParsed(AllOf( AtJsonPtr("/event", Eq("disconnect")), AtJsonPtr("/payload/pageId", Eq(std::to_string(pageId))))))) .RetiresOnSaturation(); localConnections_[0]->getRemoteConnection().onDisconnect(); EXPECT_CALL(*localConnections_[0], disconnect()).RetiresOnSaturation(); getInspectorInstance().removePage(pageId); } TEST_F(InspectorPackagerConnectionTest, TestSendReceiveEventsToMultiplePages) { // Configure gmock to expect calls in a specific order. InSequence mockCallsMustBeInSequence; packagerConnection_->connect(); std::vector pageIds; const int kNumPages = 2; for (int i = 0; i < kNumPages; ++i) { pageIds.push_back(getInspectorInstance().addPage( "mock-title", "mock-vm", localConnections_ .lazily_make_unique>())); if (i > 0) { ASSERT_NE(pageIds[i], pageIds[i - 1]) << "Received duplicate page IDs from inspector."; } } for (int i = 0; i < kNumPages; ++i) { // Connect to the i-th page. webSockets_[0]->getDelegate().didReceiveMessage(sformat( R"({{ "event": "connect", "payload": {{ "pageId": {0} }} }})", toJson(std::to_string(pageIds[i])))); ASSERT_TRUE(localConnections_[i]); } // Send an event from each LocalConnection and observe it being sent via // the socket. for (int i = 0; i < kNumPages; ++i) { // Generate a unique method name for this page to validate that we are // routing the events correctly. std::string method = "FakeDomain.eventFromPage"s + std::to_string(pageIds[i]); EXPECT_CALL( *webSockets_[0], send(JsonParsed(AllOf( AtJsonPtr("/event", Eq("wrappedEvent")), AtJsonPtr("/payload/pageId", Eq(std::to_string(pageIds[i]))), AtJsonPtr( "/payload/wrappedEvent", JsonParsed(AtJsonPtr("/method", Eq(method)))))))) .RetiresOnSaturation(); localConnections_[i]->getRemoteConnection().onMessage( toJson(dynamic::object("method", method))); } // Send an event from the frontend (remote) to the backend (local) and // observe it being received by each LocalConnection. for (int i = 0; i < kNumPages; ++i) { // Generate a unique method name for this page to validate that we are // routing the events correctly. std::string method = "FakeDomain.methodToPage"s + std::to_string(pageIds[i]); EXPECT_CALL( *localConnections_[i], sendMessage(JsonParsed(AtJsonPtr("/method", Eq(method))))) .RetiresOnSaturation(); webSockets_[0]->getDelegate().didReceiveMessage(sformat( R"({{ "event": "wrappedEvent", "payload": {{ "pageId": {0}, "wrappedEvent": {1} }} }})", toJson(std::to_string(pageIds[i])), toJson(toJson(dynamic::object("method", method))))); } for (int i = 0; i < kNumPages; ++i) { EXPECT_CALL(*localConnections_[i], disconnect()).RetiresOnSaturation(); getInspectorInstance().removePage(pageIds[i]); } } TEST_F(InspectorPackagerConnectionTest, TestSendEventToAllConnections) { // Configure gmock to expect calls in a specific order. InSequence mockCallsMustBeInSequence; packagerConnection_->connect(); auto pageId = getInspectorInstance().addPage( "mock-title", "mock-vm", localConnections_ .lazily_make_unique>()); // Connect to the page. webSockets_[0]->getDelegate().didReceiveMessage(sformat( R"({{ "event": "connect", "payload": {{ "pageId": {0} }} }})", toJson(std::to_string(pageId)))); ASSERT_TRUE(localConnections_[0]); // Impersonate the frontend (remote) to send a message to all (local) // connections. EXPECT_CALL( *localConnections_[0], sendMessage(JsonParsed(AllOf( AtJsonPtr("/method", Eq("FakeDomain.fakeMethod")), AtJsonPtr("/id", Eq(1234)), AtJsonPtr("/params", ElementsAre("arg1", "arg2")))))) .RetiresOnSaturation(); packagerConnection_->sendEventToAllConnections(R"({ "method": "FakeDomain.fakeMethod", "id": 1234, "params": ["arg1", "arg2"] })"); EXPECT_CALL(*localConnections_[0], disconnect()).RetiresOnSaturation(); getInspectorInstance().removePage(pageId); } TEST_F(InspectorPackagerConnectionTest, TestConnectThenDisconnect) { // Configure gmock to expect calls in a specific order. InSequence mockCallsMustBeInSequence; packagerConnection_->connect(); auto pageId = getInspectorInstance().addPage( "mock-title", "mock-vm", localConnections_ .lazily_make_unique>()); // Connect to the page. webSockets_[0]->getDelegate().didReceiveMessage(sformat( R"({{ "event": "connect", "payload": {{ "pageId": {0} }} }})", toJson(std::to_string(pageId)))); ASSERT_TRUE(localConnections_[0]); // Disconnect from the page. EXPECT_CALL(*localConnections_[0], disconnect()).RetiresOnSaturation(); webSockets_[0]->getDelegate().didReceiveMessage(sformat( R"({{ "event": "disconnect", "payload": {{ "pageId": {0} }} }})", toJson(std::to_string(pageId)))); EXPECT_FALSE(localConnections_[0]); } TEST_F(InspectorPackagerConnectionTest, TestConnectThenCloseSocket) { // Configure gmock to expect calls in a specific order. InSequence mockCallsMustBeInSequence; packagerConnection_->connect(); auto pageId = getInspectorInstance().addPage( "mock-title", "mock-vm", localConnections_ .lazily_make_unique>()); // Connect to the page. webSockets_[0]->getDelegate().didReceiveMessage(sformat( R"({{ "event": "connect", "payload": {{ "pageId": {0} }} }})", toJson(std::to_string(pageId)))); ASSERT_TRUE(localConnections_[0]); // Notify that the socket was closed. EXPECT_CALL(*localConnections_[0], disconnect()).RetiresOnSaturation(); webSockets_[0]->getDelegate().didClose(); EXPECT_FALSE(localConnections_[0]); } TEST_F(InspectorPackagerConnectionTest, TestConnectThenSocketFailure) { // Configure gmock to expect calls in a specific order. InSequence mockCallsMustBeInSequence; packagerConnection_->connect(); auto pageId = getInspectorInstance().addPage( "mock-title", "mock-vm", localConnections_ .lazily_make_unique>()); // Connect to the page. webSockets_[0]->getDelegate().didReceiveMessage(sformat( R"({{ "event": "connect", "payload": {{ "pageId": {0} }} }})", toJson(std::to_string(pageId)))); ASSERT_TRUE(localConnections_[0]); // Notify that the socket was closed (implicitly, as the result of an error). EXPECT_CALL(*localConnections_[0], disconnect()).RetiresOnSaturation(); webSockets_[0]->getDelegate().didFailWithError(ECONNABORTED, "Test error"); EXPECT_FALSE(localConnections_[0]); } TEST_F( InspectorPackagerConnectionTestAsync, TestExplicitCloseAfterSocketFailure) { // Configure gmock to expect calls in a specific order. InSequence mockCallsMustBeInSequence; packagerConnection_->connect(); auto pageId = getInspectorInstance().addPage( "mock-title", "mock-vm", localConnections_ .lazily_make_unique>()); // Connect to the page. webSockets_[0]->getDelegate().didReceiveMessage(sformat( R"({{ "event": "connect", "payload": {{ "pageId": {0} }} }})", toJson(std::to_string(pageId)))); ASSERT_TRUE(localConnections_[0]); // Notify that the socket was closed (implicitly, as the result of an error). EXPECT_CALL(*localConnections_[0], disconnect()).RetiresOnSaturation(); { // The WebSocket instance gets destroyed during didFailWithError, so extract // the delegate in order to call didClose. std::shared_ptr webSocketDelegate = webSockets_[0]->delegate.lock(); webSocketDelegate->didFailWithError(ECONNABORTED, "Test error"); webSocketDelegate->didClose(); } EXPECT_FALSE(localConnections_[0]); // We're still disconnected since we haven't called the reconnect callback. EXPECT_FALSE(packagerConnection_->isConnected()); // Flush the callback queue. asyncExecutor_.advance(2000ms); EXPECT_TRUE(packagerConnection_->isConnected()); } TEST_F( InspectorPackagerConnectionTest, TestConnectWhileAlreadyConnectedCausesDisconnection) { // Configure gmock to expect calls in a specific order. InSequence mockCallsMustBeInSequence; packagerConnection_->connect(); auto pageId = getInspectorInstance().addPage( "mock-title", "mock-vm", localConnections_ .lazily_make_unique>()); // Connect to the page. webSockets_[0]->getDelegate().didReceiveMessage(sformat( R"({{ "event": "connect", "payload": {{ "pageId": {0} }} }})", toJson(std::to_string(pageId)))); ASSERT_TRUE(localConnections_[0]); // Try connecting to the same page again. This results in a disconnection. EXPECT_CALL(*localConnections_[0], disconnect()).RetiresOnSaturation(); webSockets_[0]->getDelegate().didReceiveMessage(sformat( R"({{ "event": "connect", "payload": {{ "pageId": {0} }} }})", toJson(std::to_string(pageId)))); EXPECT_FALSE(localConnections_[0]); } TEST_F(InspectorPackagerConnectionTest, TestMultipleDisconnect) { // Configure gmock to expect calls in a specific order. InSequence mockCallsMustBeInSequence; packagerConnection_->connect(); auto pageId = getInspectorInstance().addPage( "mock-title", "mock-vm", localConnections_ .lazily_make_unique>()); // Connect to the page. webSockets_[0]->getDelegate().didReceiveMessage(sformat( R"({{ "event": "connect", "payload": {{ "pageId": {0} }} }})", toJson(std::to_string(pageId)))); ASSERT_TRUE(localConnections_[0]); // Disconnect from the page. EXPECT_CALL(*localConnections_[0], disconnect()).RetiresOnSaturation(); webSockets_[0]->getDelegate().didReceiveMessage(sformat( R"({{ "event": "disconnect", "payload": {{ "pageId": {0} }} }})", toJson(std::to_string(pageId)))); EXPECT_FALSE(localConnections_[0]); // Disconnect again. This is a noop. webSockets_[0]->getDelegate().didReceiveMessage(sformat( R"({{ "event": "disconnect", "payload": {{ "pageId": {0} }} }})", toJson(std::to_string(pageId)))); EXPECT_FALSE(localConnections_[0]); } TEST_F(InspectorPackagerConnectionTest, TestDisconnectThenSendEvent) { // Configure gmock to expect calls in a specific order. InSequence mockCallsMustBeInSequence; packagerConnection_->connect(); auto pageId = getInspectorInstance().addPage( "mock-title", "mock-vm", localConnections_ .lazily_make_unique>()); // Connect to the page. webSockets_[0]->getDelegate().didReceiveMessage(sformat( R"({{ "event": "connect", "payload": {{ "pageId": {0} }} }})", toJson(std::to_string(pageId)))); ASSERT_TRUE(localConnections_[0]); // Disconnect from the page. EXPECT_CALL(*localConnections_[0], disconnect()).RetiresOnSaturation(); webSockets_[0]->getDelegate().didReceiveMessage(sformat( R"({{ "event": "disconnect", "payload": {{ "pageId": {0} }} }})", toJson(std::to_string(pageId)))); EXPECT_FALSE(localConnections_[0]); // Send an event from the frontend (remote) to the backend (local). This // is a noop. webSockets_[0]->getDelegate().didReceiveMessage(sformat( R"({{ "event": "wrappedEvent", "payload": {{ "pageId": {0}, "wrappedEvent": {1} }} }})", toJson(std::to_string(pageId)), toJson(R"({ "method": "FakeDomain.fakeMethod", "id": 1234, "params": ["arg1", "arg2"] })"))); } TEST_F(InspectorPackagerConnectionTest, TestSendEventToUnknownPage) { // Configure gmock to expect calls in a specific order. InSequence mockCallsMustBeInSequence; packagerConnection_->connect(); // Send an event from the frontend (remote) to the backend (local). This // is a noop (except for logging). webSockets_[0]->getDelegate().didReceiveMessage(sformat( R"({{ "event": "wrappedEvent", "payload": {{ "pageId": "1234", "wrappedEvent": {0} }} }})", toJson(R"({ "method": "FakeDomain.fakeMethod", "id": 1234, "params": ["arg1", "arg2"] })"))); } TEST_F(InspectorPackagerConnectionTest, TestReconnectSuccessful) { // Configure gmock to expect calls in a specific order. InSequence mockCallsMustBeInSequence; packagerConnection_->connect(); ASSERT_TRUE(webSockets_[0]); EXPECT_CALL(*packagerConnectionDelegate(), scheduleCallback(_, _)) .RetiresOnSaturation(); webSockets_[0]->getDelegate().didClose(); EXPECT_FALSE(webSockets_[0]); EXPECT_TRUE(webSockets_[1]); EXPECT_TRUE(packagerConnection_->isConnected()); // Stops attempting to reconnect after closeQuietly packagerConnection_->closeQuietly(); } TEST_F(InspectorPackagerConnectionTest, TestReconnectFailure) { // Configure gmock to expect calls in a specific order. InSequence mockCallsMustBeInSequence; packagerConnection_->connect(); ASSERT_TRUE(webSockets_[0]); EXPECT_CALL(*packagerConnectionDelegate(), scheduleCallback(_, _)) .Times(2) .RetiresOnSaturation(); webSockets_[0]->getDelegate().didClose(); EXPECT_FALSE(webSockets_[0]); ASSERT_TRUE(webSockets_[1]); webSockets_[1]->getDelegate().didClose(); EXPECT_FALSE(webSockets_[1]); ASSERT_TRUE(webSockets_[2]); EXPECT_TRUE(packagerConnection_->isConnected()); // Stops attempting to reconnect after closeQuietly packagerConnection_->closeQuietly(); EXPECT_FALSE(webSockets_[2]); EXPECT_FALSE(packagerConnection_->isConnected()); } TEST_F(InspectorPackagerConnectionTest, TestReconnectOnSocketError) { // Configure gmock to expect calls in a specific order. InSequence mockCallsMustBeInSequence; packagerConnection_->connect(); ASSERT_TRUE(webSockets_[0]); EXPECT_CALL(*packagerConnectionDelegate(), scheduleCallback(_, _)) .RetiresOnSaturation(); webSockets_[0]->getDelegate().didFailWithError(ECONNRESET, "Test error"); EXPECT_FALSE(webSockets_[0]); EXPECT_TRUE(webSockets_[1]); EXPECT_TRUE(packagerConnection_->isConnected()); // Stops attempting to reconnect after closeQuietly packagerConnection_->closeQuietly(); } TEST_F(InspectorPackagerConnectionTest, TestReconnectOnSocketErrorWithNoCode) { // Configure gmock to expect calls in a specific order. InSequence mockCallsMustBeInSequence; packagerConnection_->connect(); ASSERT_TRUE(webSockets_[0]); EXPECT_CALL(*packagerConnectionDelegate(), scheduleCallback(_, _)) .RetiresOnSaturation(); webSockets_[0]->getDelegate().didFailWithError(std::nullopt, "Test error"); EXPECT_FALSE(webSockets_[0]); EXPECT_TRUE(webSockets_[1]); EXPECT_TRUE(packagerConnection_->isConnected()); // Stops attempting to reconnect after closeQuietly packagerConnection_->closeQuietly(); } TEST_F(InspectorPackagerConnectionTest, TestNoReconnectOnConnectionRefused) { // Configure gmock to expect calls in a specific order. InSequence mockCallsMustBeInSequence; packagerConnection_->connect(); ASSERT_TRUE(webSockets_[0]); webSockets_[0]->getDelegate().didFailWithError(ECONNREFUSED, "Test error"); EXPECT_FALSE(webSockets_[0]); EXPECT_FALSE(packagerConnection_->isConnected()); } TEST_F(InspectorPackagerConnectionTest, TestUnknownEvent) { packagerConnection_->connect(); ASSERT_TRUE(webSockets_[0]); // This is a noop (other than logging an error). webSockets_[0]->getDelegate().didReceiveMessage(R"({"event": "foo"})"); } TEST_F(InspectorPackagerConnectionTest, TestMalformedEvent) { packagerConnection_->connect(); ASSERT_TRUE(webSockets_[0]); // This is a noop (other than logging an error). webSockets_[0]->getDelegate().didReceiveMessage("this is not json"); webSockets_[0]->getDelegate().didReceiveMessage("{"); webSockets_[0]->getDelegate().didReceiveMessage(""); } TEST_F(InspectorPackagerConnectionTest, TestEventsNotConformingToType) { packagerConnection_->connect(); ASSERT_TRUE(webSockets_[0]); // These are all noops (other than logging an error). webSockets_[0]->getDelegate().didReceiveMessage(R"({})"); webSockets_[0]->getDelegate().didReceiveMessage( R"({"event": "wrappedEvent"})"); webSockets_[0]->getDelegate().didReceiveMessage(R"({"event": "connect"})"); webSockets_[0]->getDelegate().didReceiveMessage(R"({"event": "disconnect"})"); webSockets_[0]->getDelegate().didReceiveMessage( R"({"payload": {"pageId": "1"}})"); } TEST_F( InspectorPackagerConnectionTest, TestWebSocketDelegateIsDestroyedWithConnectionByDefault) { packagerConnection_->connect(); ASSERT_TRUE(webSockets_[0]); std::weak_ptr delegate = webSockets_[0]->delegate; EXPECT_TRUE(delegate.lock()); packagerConnection_.reset(); EXPECT_FALSE(delegate.lock()); } // Edge case: When the C++ layer has released the InspectorPackagerConnection, // the platform bindings can still call methods on IWebSocketDelegate through // a shared_ptr (typically _briefly_ upgraded from the weak_ptr we provide). TEST_F( InspectorPackagerConnectionTest, TestWebSocketDelegateCanOutliveConnection) { // Configure gmock to expect calls in a specific order. InSequence mockCallsMustBeInSequence; packagerConnection_->connect(); ASSERT_TRUE(webSockets_[0]); std::shared_ptr retainedWebSocketDelegate = webSockets_[0]->delegate.lock(); ASSERT_TRUE(retainedWebSocketDelegate); // Destroy our InspectorPackagerConnection. We can't call methods on it // anymore, but its internals are still valid and it is still responding to // socket messages. packagerConnection_.reset(); auto pageId = getInspectorInstance().addPage( "mock-title", "mock-vm", localConnections_ .lazily_make_unique>()); // Connect to the page. retainedWebSocketDelegate->didReceiveMessage(sformat( R"({{ "event": "connect", "payload": {{ "pageId": {0} }} }})", toJson(std::to_string(pageId)))); ASSERT_TRUE(localConnections_[0]); // Send an event from the frontend (remote) to the backend (local) and // observe it being received by localConnection. EXPECT_CALL( *localConnections_[0], sendMessage(JsonParsed(AllOf( AtJsonPtr("/method", Eq("FakeDomain.fakeMethod")), AtJsonPtr("/id", Eq(1234)), AtJsonPtr("/params", ElementsAre("arg1", "arg2")))))) .RetiresOnSaturation(); retainedWebSocketDelegate->didReceiveMessage(sformat( R"({{ "event": "wrappedEvent", "payload": {{ "pageId": {0}, "wrappedEvent": {1} }} }})", toJson(std::to_string(pageId)), toJson(R"({ "method": "FakeDomain.fakeMethod", "id": 1234, "params": ["arg1", "arg2"] })"))); retainedWebSocketDelegate.reset(); EXPECT_FALSE(localConnections_[0]); EXPECT_FALSE(webSockets_[0]); } TEST_F(InspectorPackagerConnectionTest, TestDestroyConnectionOnPageRemoved) { // Configure gmock to expect calls in a specific order. InSequence mockCallsMustBeInSequence; packagerConnection_->connect(); ASSERT_TRUE(webSockets_[0]); auto pageId = getInspectorInstance().addPage( "mock-title", "mock-vm", localConnections_ .lazily_make_unique>()); // Connect to the page. webSockets_[0]->getDelegate().didReceiveMessage(sformat( R"({{ "event": "connect", "payload": {{ "pageId": {0} }} }})", toJson(std::to_string(pageId)))); EXPECT_TRUE(localConnections_[0]); // Remove the page. EXPECT_CALL(*localConnections_[0], disconnect()).RetiresOnSaturation(); getInspectorInstance().removePage(pageId); EXPECT_FALSE(localConnections_[0]); } TEST_F( InspectorPackagerConnectionTestAsync, TestAttemptSendToRemoteAfterDestroyed) { // Configure gmock to expect calls in a specific order. InSequence mockCallsMustBeInSequence; packagerConnection_->connect(); auto pageId = getInspectorInstance().addPage( "mock-title", "mock-vm", localConnections_ .lazily_make_unique>()); // Connect to the page. webSockets_[0]->getDelegate().didReceiveMessage(sformat( R"({{ "event": "connect", "payload": {{ "pageId": {0} }} }})", toJson(std::to_string(pageId)))); ASSERT_TRUE(localConnections_[0]); // Send an event from the mocked backend (local) to the frontend (remote) // but don't flush the callback queue yet. localConnections_[0]->getRemoteConnection().onMessage(R"({ "method": "FakeDomain.eventTriggered", "params": ["arg1", "arg2"] })"); EXPECT_CALL(*localConnections_[0], disconnect()).RetiresOnSaturation(); getInspectorInstance().removePage(pageId); packagerConnection_.reset(); // Flush the callback queue. This doesn't crash. EXPECT_EQ(asyncExecutor_.run(), 1); } TEST_F( InspectorPackagerConnectionTestAsync, TestAttemptSendToStaleRemoteConnection) { // Configure gmock to expect calls in a specific order. InSequence mockCallsMustBeInSequence; packagerConnection_->connect(); auto pageId = getInspectorInstance().addPage( "mock-title", "mock-vm", localConnections_ .lazily_make_unique>()); // Connect to the page. webSockets_[0]->getDelegate().didReceiveMessage(sformat( R"({{ "event": "connect", "payload": {{ "pageId": {0} }} }})", toJson(std::to_string(pageId)))); ASSERT_TRUE(localConnections_[0]); // Send an event from the mocked backend (local) to the frontend (remote) // but don't flush the callback queue yet. localConnections_[0]->getRemoteConnection().onMessage(R"({ "method": "FakeDomain.eventToBeDropped", "params": ["arg1", "arg2"] })"); // Disconnect from the page. EXPECT_CALL(*localConnections_[0], disconnect()).RetiresOnSaturation(); webSockets_[0]->getDelegate().didReceiveMessage(sformat( R"({{ "event": "disconnect", "payload": {{ "pageId": {0} }} }})", toJson(std::to_string(pageId)))); EXPECT_FALSE(localConnections_[0]); // Connect to the same page again. webSockets_[0]->getDelegate().didReceiveMessage(sformat( R"({{ "event": "connect", "payload": {{ "pageId": {0} }} }})", toJson(std::to_string(pageId)))); EXPECT_TRUE(localConnections_[1]); // Send an event from the mocked backend (local) to the frontend (remote) over // the new connection, then flush the callback queue. // Only this event should be sent over the socket. EXPECT_CALL( *webSockets_[0], send(JsonParsed(AllOf( AtJsonPtr("/event", Eq("wrappedEvent")), AtJsonPtr("/payload/pageId", Eq(std::to_string(pageId))), AtJsonPtr( "/payload/wrappedEvent", JsonEq( R"({ "method": "FakeDomain.eventToBeDelivered", "params": ["arg1", "arg2"] })")))))) .RetiresOnSaturation(); localConnections_[1]->getRemoteConnection().onMessage(R"({ "method": "FakeDomain.eventToBeDelivered", "params": ["arg1", "arg2"] })"); EXPECT_EQ(asyncExecutor_.run(), 2); // Clean up. EXPECT_CALL(*localConnections_[1], disconnect()).RetiresOnSaturation(); getInspectorInstance().removePage(pageId); } TEST_F( InspectorPackagerConnectionTestAsync, TestAttemptSendToStaleRemoteConnectionWhenRetained) { // Configure gmock to expect calls in a specific order. InSequence mockCallsMustBeInSequence; packagerConnection_->connect(); auto pageId = getInspectorInstance().addPage( "mock-title", "mock-vm", localConnections_ .lazily_make_unique>()); // Connect to the page. webSockets_[0]->getDelegate().didReceiveMessage(sformat( R"({{ "event": "connect", "payload": {{ "pageId": {0} }} }})", toJson(std::to_string(pageId)))); ASSERT_TRUE(localConnections_[0]); // Send an event from the mocked backend (local) to the frontend (remote) // but don't flush the callback queue yet. localConnections_[0]->getRemoteConnection().onMessage(R"({ "method": "FakeDomain.eventToBeDropped", "params": ["arg1", "arg2"] })"); // Forcibly retain the remote connection beyond localConnections_[0]'s // lifetime. auto retainedRemoteConnection0 = localConnections_[0]->dangerouslyReleaseRemoteConnection(); // Disconnect from the page. EXPECT_CALL(*localConnections_[0], disconnect()).RetiresOnSaturation(); webSockets_[0]->getDelegate().didReceiveMessage(sformat( R"({{ "event": "disconnect", "payload": {{ "pageId": {0} }} }})", toJson(std::to_string(pageId)))); EXPECT_FALSE(localConnections_[0]); // Connect to the same page again. webSockets_[0]->getDelegate().didReceiveMessage(sformat( R"({{ "event": "connect", "payload": {{ "pageId": {0} }} }})", toJson(std::to_string(pageId)))); EXPECT_TRUE(localConnections_[1]); // Remember localConnections_[0]'s remote connection? We can still use it // without crashing, but it will not deliver any messages. retainedRemoteConnection0->onMessage(R"({ "method": "FakeDomain.anotherEventToBeDropped", "params": ["arg1", "arg2"] })"); retainedRemoteConnection0->onDisconnect(); // Send events from the mocked backend (local) to the frontend (remote) over // the new connection, then flush the callback queue. // Only these events should be sent over the socket. EXPECT_CALL( *webSockets_[0], send(JsonParsed(AllOf( AtJsonPtr("/event", Eq("wrappedEvent")), AtJsonPtr("/payload/pageId", Eq(std::to_string(pageId))), AtJsonPtr( "/payload/wrappedEvent", JsonEq( R"({ "method": "FakeDomain.eventToBeDelivered", "params": ["arg1", "arg2"] })")))))) .RetiresOnSaturation(); localConnections_[1]->getRemoteConnection().onMessage(R"({ "method": "FakeDomain.eventToBeDelivered", "params": ["arg1", "arg2"] })"); EXPECT_CALL( *webSockets_[0], send(JsonParsed(AllOf( AtJsonPtr("/event", Eq("disconnect")), AtJsonPtr("/payload/pageId", Eq(std::to_string(pageId))))))) .RetiresOnSaturation(); localConnections_[1]->getRemoteConnection().onDisconnect(); EXPECT_EQ(asyncExecutor_.run(), 5); // Clean up. EXPECT_CALL(*localConnections_[1], disconnect()).RetiresOnSaturation(); getInspectorInstance().removePage(pageId); } TEST_F(InspectorPackagerConnectionTest, TestRejectedPageConnection) { // Configure gmock to expect calls in a specific order. InSequence mockCallsMustBeInSequence; enum { Accept, RejectSilently, RejectWithDisconnect } mockNextConnectionBehavior; auto pageId = getInspectorInstance().addPage( "mock-title", "mock-vm", [&mockNextConnectionBehavior, this](auto remoteConnection) -> std::unique_ptr { switch (mockNextConnectionBehavior) { case Accept: return localConnections_.make_unique(std::move(remoteConnection)); case RejectSilently: return nullptr; case RejectWithDisconnect: remoteConnection->onDisconnect(); return nullptr; } }); packagerConnection_->connect(); ASSERT_TRUE(webSockets_[0]); // Reject the connection by returning nullptr. mockNextConnectionBehavior = RejectSilently; EXPECT_CALL( *webSockets_[0], send(JsonParsed(AllOf( AtJsonPtr("/event", Eq("disconnect")), AtJsonPtr("/payload/pageId", Eq(std::to_string(pageId))))))) .RetiresOnSaturation(); webSockets_[0]->getDelegate().didReceiveMessage(sformat( R"({{ "event": "connect", "payload": {{ "pageId": {0} }} }})", toJson(std::to_string(pageId)))); webSockets_[0]->getDelegate().didReceiveMessage(sformat( R"({{ "event": "wrappedEvent", "payload": {{ "pageId": {0}, "wrappedEvent": {1} }} }})", toJson(std::to_string(pageId)), toJson(R"({ "method": "FakeDomain.fakeMethod", "id": 1, "params": ["arg1", "arg2"] })"))); // Reject the connection by explicitly calling onDisconnect(), then returning // nullptr. mockNextConnectionBehavior = RejectWithDisconnect; EXPECT_CALL( *webSockets_[0], send(JsonParsed(AllOf( AtJsonPtr("/event", Eq("disconnect")), AtJsonPtr("/payload/pageId", Eq(std::to_string(pageId))))))) .RetiresOnSaturation(); webSockets_[0]->getDelegate().didReceiveMessage(sformat( R"({{ "event": "connect", "payload": {{ "pageId": {0} }} }})", toJson(std::to_string(pageId)))); webSockets_[0]->getDelegate().didReceiveMessage(sformat( R"({{ "event": "wrappedEvent", "payload": {{ "pageId": {0}, "wrappedEvent": {1} }} }})", toJson(std::to_string(pageId)), toJson(R"({ "method": "FakeDomain.fakeMethod", "id": 2, "params": ["arg1", "arg2"] })"))); // Accept a connection after previously rejecting connections to the same // page. mockNextConnectionBehavior = Accept; webSockets_[0]->getDelegate().didReceiveMessage(sformat( R"({{ "event": "connect", "payload": {{ "pageId": {0} }} }})", toJson(std::to_string(pageId)))); EXPECT_CALL( *localConnections_[0], sendMessage(JsonParsed(AllOf( AtJsonPtr("/method", Eq("FakeDomain.fakeMethod")), AtJsonPtr("/id", Eq(3)), AtJsonPtr("/params", ElementsAre("arg1", "arg2")))))) .RetiresOnSaturation(); webSockets_[0]->getDelegate().didReceiveMessage(sformat( R"({{ "event": "wrappedEvent", "payload": {{ "pageId": {0}, "wrappedEvent": {1} }} }})", toJson(std::to_string(pageId)), toJson(R"({ "method": "FakeDomain.fakeMethod", "id": 3, "params": ["arg1", "arg2"] })"))); EXPECT_CALL(*localConnections_[0], disconnect()).RetiresOnSaturation(); getInspectorInstance().removePage(pageId); } } // namespace facebook::react::jsinspector_modern