/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */ /* * Copyright the Collabora Online contributors. * * SPDX-License-Identifier: MPL-2.0 * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include #include #include "WebSocketSession.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace helpers; /// Tests the HTTP WebSocket API of coolwsd. The server has to be started manually before running this test. class HTTPWSTest : public CPPUNIT_NS::TestFixture { const Poco::URI _uri; Poco::Net::HTTPResponse _response; std::shared_ptr _socketPoll; CPPUNIT_TEST_SUITE(HTTPWSTest); CPPUNIT_TEST(testExoticLang); CPPUNIT_TEST(testSaveOnDisconnect); CPPUNIT_TEST(testReloadWhileDisconnecting); CPPUNIT_TEST(testInactiveClient); CPPUNIT_TEST(testViewInfoMsg); CPPUNIT_TEST(testUndoConflict); CPPUNIT_TEST_SUITE_END(); void testExoticLang(); void testSaveOnDisconnect(); void testReloadWhileDisconnecting(); void testInactiveClient(); void testViewInfoMsg(); void testUndoConflict(); public: HTTPWSTest() : _uri(helpers::getTestServerURI()) , _socketPoll(std::make_shared("HttpWsPoll")) { #if ENABLE_SSL Poco::Net::initializeSSL(); // Just accept the certificate anyway for testing purposes Poco::SharedPtr invalidCertHandler = new Poco::Net::AcceptCertificateHandler(false); Poco::Net::Context::Params sslParams; Poco::Net::Context::Ptr sslContext = new Poco::Net::Context(Poco::Net::Context::CLIENT_USE, sslParams); Poco::Net::SSLManager::instance().initializeClient(nullptr, std::move(invalidCertHandler), std::move(sslContext)); #endif } ~HTTPWSTest() { #if ENABLE_SSL Poco::Net::uninitializeSSL(); #endif } void setUp() { resetTestStartTime(); waitForKitPidsReady("setUp"); resetTestStartTime(); _socketPoll->startThread(); } void tearDown() { _socketPoll->joinThread(); resetTestStartTime(); waitForKitPidsReady("tearDown"); resetTestStartTime(); } }; void HTTPWSTest::testExoticLang() { const std::string testname = "testExoticLang- "; std::string documentPath, documentURL; getDocumentPathAndURL("hello.odt", documentPath, documentURL, testname); try { // es-419 locale takes significantly longer than the default. // 22+ seconds has been observed. const auto timeout = std::chrono::seconds(COMMAND_TIMEOUT_SECS * 6); std::shared_ptr socket = loadDocAndGetSession( _socketPoll, _uri, documentURL, "exoticlocale", /*isView=*/true, /*isAssert=*/true, /*loadParams=*/" lang=es-419", timeout); socket->asyncShutdown(); } catch (const Poco::Exception& exc) { LOK_ASSERT_FAIL(exc.displayText()); } } void HTTPWSTest::testSaveOnDisconnect() { const std::string testname = "saveOnDisconnect- "; const std::string text = helpers::genRandomString(40); TST_LOG("Test string: [" << text << "]."); std::string documentPath, documentURL; getDocumentPathAndURL("hello.odt", documentPath, documentURL, testname); try { std::shared_ptr socket1 = loadDocAndGetSession(_socketPoll, _uri, documentURL, testname + "1 "); std::shared_ptr socket2 = loadDocAndGetSession(_socketPoll, _uri, documentURL, testname + "2 "); sendTextFrame(socket2, "userinactive"); deleteAll(socket1, testname, std::chrono::milliseconds(100), 1); sendTextFrame(socket1, "paste mimetype=text/plain;charset=utf-8\n" + text, testname); getResponseMessage(socket1, "pasteresult: success", testname); // Shutdown abruptly. TST_LOG("Closing connection after pasting."); socket1->asyncShutdown(); // Should trigger saving. socket2->asyncShutdown(); LOK_ASSERT_MESSAGE("Expected successful disconnection of the WebSocket 1", socket1->waitForDisconnection(std::chrono::seconds(5))); LOK_ASSERT_MESSAGE("Expected successful disconnection of the WebSocket 2", socket2->waitForDisconnection(std::chrono::seconds(5))); // Allow time to save and destroy before we connect again. waitForKitPidsReady(testname); TST_LOG("Loading again."); // Load the same document and check that the last changes (pasted text) is saved. std::shared_ptr socket = loadDocAndGetSession(_socketPoll, _uri, documentURL, testname + "3 "); // Check if the document contains the pasted text. const std::string selection = getAllText(socket, testname, text); LOK_ASSERT_EQUAL("textselectioncontent: " + text, selection); socket->asyncShutdown(); LOK_ASSERT_MESSAGE("Expected successful disconnection of the WebSocket 3", socket->waitForDisconnection(std::chrono::seconds(5))); } catch (const Poco::Exception& exc) { LOK_ASSERT_FAIL(exc.displayText()); } } void HTTPWSTest::testReloadWhileDisconnecting() { const char* testname = "reloadWhileDisconnecting "; try { std::string documentPath, documentURL; getDocumentPathAndURL("hello.odt", documentPath, documentURL, testname); std::shared_ptr socket = loadDocAndGetSession(_socketPoll, _uri, documentURL, testname); deleteAll(socket, testname); sendTextFrame(socket, "paste mimetype=text/plain;charset=utf-8\naaa bbb ccc", testname); // Closing connection too fast might not flush buffers. // Often nothing more than the SelectAll reaches the server before // the socket is closed, when the doc is not even modified yet. getResponseMessage(socket, "statechanged", testname); // Shutdown abruptly. TST_LOG("Closing connection after pasting."); socket->asyncShutdown(); LOK_ASSERT_MESSAGE("Expected successful disconnection of the WebSocket", socket->waitForDisconnection(std::chrono::seconds(5))); // Do not wait here. Reconnect before disconnect finishes // TODO: Test fails because it is unable to reconnect // Load the same document and check that the last changes (pasted text) is saved. TST_LOG("Loading again."); socket = loadDocAndGetSession(_socketPoll, _uri, documentURL, testname); TST_LOG("Checking if the document contains the pasted text."); const std::string expected = "aaa bbb ccc"; const std::string selection = getAllText(socket, testname, expected); LOK_ASSERT_EQUAL(std::string("textselectioncontent: ") + expected, selection); TST_LOG("Closing connection after getting pasted text."); socket->asyncShutdown(); LOK_ASSERT_MESSAGE("Expected successful disconnection of the WebSocket", socket->waitForDisconnection(std::chrono::seconds(5))); } catch (const Poco::Exception& exc) { LOK_ASSERT_FAIL(exc.displayText()); } } void HTTPWSTest::testInactiveClient() { const char* testname = "inactiveClient "; try { std::string documentPath, documentURL; getDocumentPathAndURL("hello.odt", documentPath, documentURL, testname); std::shared_ptr socket1 = loadDocAndGetSession(_socketPoll, _uri, documentURL, "inactiveClient-1 "); // Connect another and go inactive. TST_LOG_NAME("inactiveClient-2 ", "Connecting second client."); std::shared_ptr socket2 = loadDocAndGetSession(_socketPoll, _uri, documentURL, "inactiveClient-2 ", true); sendTextFrame(socket2, "userinactive", "inactiveClient-2 "); // While second is inactive, make some changes. sendTextFrame(socket1, "key type=input char=97 key=0", "inactiveClient-1 "); sendTextFrame(socket1, "key type=up char=0 key=512", "inactiveClient-1 "); // Activate second. sendTextFrame(socket2, "useractive", "inactiveClient-2 "); SocketProcessor("Second ", socket2, [&](const std::string& msg) { const auto token = COOLProtocol::getFirstToken(msg); // 'window:' is e.g. 'window: {"id":"4","action":"invalidate","rectangle":"0, 0, // 0, 0"}', which is probably fine, given that other invalidations are also // expected. LOK_ASSERT_MESSAGE("unexpected message: " + msg, token == "a11yfocuschanged:" || token == "corelog:" || token == "cursorvisible:" || token == "graphicselection:" || token == "graphicviewselection:" || token == "invalidatecursor:" || token == "invalidatetiles:" || token == "invalidateviewcursor:" || token == "setpart:" || token == "statechanged:" || token == "textselection:" || token == "textselectionend:" || token == "textselectionstart:" || token == "textviewselection:" || token == "viewcursorvisible:" || token == "viewinfo:" || token == "editor:" || token == "context:" || token == "window:" || token == "hrulerupdate:" || token == "vrulerupdate:" || token == "tableselected:" || token == "colorpalettes:" || token == "jsdialog:"); // End when we get state changed. return (token != "statechanged:"); }); TST_LOG("Second client finished. Shutting down"); socket2->asyncShutdown(); TST_LOG("Shutting down first client"); socket1->asyncShutdown(); LOK_ASSERT_MESSAGE("Expected successful disconnection of the WebSocket 2", socket2->waitForDisconnection(std::chrono::seconds(5))); LOK_ASSERT_MESSAGE("Expected successful disconnection of the WebSocket 1", socket1->waitForDisconnection(std::chrono::seconds(5))); } catch (const Poco::Exception& exc) { LOK_ASSERT_FAIL(exc.displayText()); } } void HTTPWSTest::testViewInfoMsg() { // Load 2 documents, cross-check the viewid received by each of them in their status message // with the one sent in viewinfo message to itself as well as to other one const std::string testname = "testViewInfoMsg-"; std::string docPath; std::string docURL; getDocumentPathAndURL("hello.odt", docPath, docURL, testname); std::shared_ptr socket0 = connectLOKit(_socketPoll, _uri, docURL, testname + "0 "); std::shared_ptr socket1 = connectLOKit(_socketPoll, _uri, docURL, testname + "1 "); std::string response; int part, parts, width, height; int viewid[2] = { 0 }; try { // Load first view and remember the viewid TST_LOG("Loading the first view"); sendTextFrame(socket0, "load url=" + docURL); response = getResponseString(socket0, "status:", testname + "0 "); LOK_ASSERT_MESSAGE("Expected status: message", !response.empty()); parseDocSize(response.substr(7), "text", part, parts, width, height, viewid[0], testname); // Check if viewinfo message also mentions the same viewid TST_LOG("Waiting for [viewinfo:] from the first view"); response = getResponseString(socket0, "viewinfo: ", testname + "0 "); Poco::JSON::Parser parser0; Poco::JSON::Array::Ptr array = parser0.parse(response.substr(9)).extract(); LOK_ASSERT_EQUAL(static_cast(1), array->size()); Poco::JSON::Object::Ptr viewInfoObj0 = array->getObject(0); int viewid0 = viewInfoObj0->get("id").convert(); LOK_ASSERT_EQUAL(viewid[0], viewid0); // Load second view and remember the viewid TST_LOG("Loading the second view"); sendTextFrame(socket1, "load url=" + docURL); response = getResponseString(socket1, "status:", testname + "1 "); parseDocSize(response.substr(7), "text", part, parts, width, height, viewid[1], testname); // Check if viewinfo message in this view mentions // viewid of both first loaded view and this view TST_LOG("Waiting for [viewinfo:] from the second view"); response = getResponseString(socket1, "viewinfo: ", testname + "1 "); Poco::JSON::Parser parser1; array = parser1.parse(response.substr(9)).extract(); LOK_ASSERT_EQUAL(static_cast(2), array->size()); viewInfoObj0 = array->getObject(0); Poco::JSON::Object::Ptr viewInfoObj1 = array->getObject(1); viewid0 = viewInfoObj0->get("id").convert(); int viewid1 = viewInfoObj1->get("id").convert(); if (viewid[0] == viewid0) LOK_ASSERT_EQUAL(viewid[1], viewid1); else if (viewid[0] == viewid1) LOK_ASSERT_EQUAL(viewid[1], viewid0); else LOK_ASSERT_FAIL("Inconsistent viewid in viewinfo and status messages"); // Check if first view also got the same viewinfo message TST_LOG("Waiting for [viewinfo:] from the first view, again"); const auto response1 = getResponseString(socket0, "viewinfo: ", testname + "0 "); LOK_ASSERT_EQUAL(response, response1); socket1->asyncShutdown(); socket0->asyncShutdown(); LOK_ASSERT_MESSAGE("Expected successful disconnection of the WebSocket 1", socket1->waitForDisconnection(std::chrono::seconds(5))); LOK_ASSERT_MESSAGE("Expected successful disconnection of the WebSocket 0", socket0->waitForDisconnection(std::chrono::seconds(5))); } catch(const Poco::Exception& exc) { LOK_ASSERT_FAIL(exc.displayText()); } TST_LOG("Done"); } void HTTPWSTest::testUndoConflict() { const std::string testname = "testUndoConflict-"; Poco::JSON::Parser parser; std::string docPath; std::string docURL; int conflict = 0; getDocumentPathAndURL("empty.odt", docPath, docURL, testname); std::shared_ptr socket0 = connectLOKit(_socketPoll, _uri, docURL, testname + "0 "); std::shared_ptr socket1 = connectLOKit(_socketPoll, _uri, docURL, testname + "1 "); std::string response; try { // Load first view sendTextFrame(socket0, "load url=" + docURL, testname + "0 "); response = getResponseString(socket0, "invalidatecursor:", testname + "0 "); // Load second view sendTextFrame(socket1, "load url=" + docURL, testname + "1 "); response = getResponseString(socket1, "invalidatecursor:", testname + "1 "); // edit first view sendChar(socket0, 'A', skNone, testname + "0 "); response = getResponseString(socket0, "invalidateviewcursor: ", testname + "0 "); response = getResponseString(socket0, "invalidateviewcursor: ", testname + "0 "); // edit second view sendChar(socket1, 'B', skNone, testname + "1 "); response = getResponseString(socket1, "invalidateviewcursor: ", testname + "1 "); response = getResponseString(socket1, "invalidateviewcursor: ", testname + "1 "); // try to undo first view sendTextFrame(socket0, "uno .uno:Undo", testname + "0 "); // undo conflict response = getResponseString(socket0, "unocommandresult:", testname + "0 "); Poco::JSON::Object::Ptr objJSON = parser.parse(response.substr(17)).extract(); LOK_ASSERT_EQUAL(std::string(".uno:Undo"), objJSON->get("commandName").toString()); LOK_ASSERT_EQUAL(std::string("true"), objJSON->get("success").toString()); LOK_ASSERT(objJSON->has("result")); const Poco::Dynamic::Var parsedResultJSON = objJSON->get("result"); const auto& resultObj = parsedResultJSON.extract(); LOK_ASSERT_EQUAL(std::string("long"), resultObj->get("type").toString()); LOK_ASSERT(Poco::strToInt(resultObj->get("value").toString(), conflict, 10)); LOK_ASSERT(conflict > 0); /*UNDO_CONFLICT*/ socket1->asyncShutdown(); socket0->asyncShutdown(); LOK_ASSERT_MESSAGE("Expected successful disconnection of the WebSocket 1", socket1->waitForDisconnection(std::chrono::seconds(5))); LOK_ASSERT_MESSAGE("Expected successful disconnection of the WebSocket 0", socket0->waitForDisconnection(std::chrono::seconds(5))); } catch(const Poco::Exception& exc) { LOK_ASSERT_FAIL(exc.displayText()); } } CPPUNIT_TEST_SUITE_REGISTRATION(HTTPWSTest); /* vim:set shiftwidth=4 softtabstop=4 expandtab: */