collabora-online/test/httpwstest.cpp

462 lines
18 KiB
C++

/* -*- 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 <chrono>
#include <config.h>
#include "WebSocketSession.hpp"
#include <algorithm>
#include <vector>
#include <Poco/Net/AcceptCertificateHandler.h>
#include <Poco/Net/HTTPRequest.h>
#include <Poco/Net/HTTPResponse.h>
#include <Poco/Net/InvalidCertificateHandler.h>
#include <Poco/Net/SSLManager.h>
#include <Poco/RegularExpression.h>
#include <Poco/URI.h>
#include <cppunit/extensions/HelperMacros.h>
#include <Common.hpp>
#include <Protocol.hpp>
#include <lokassert.hpp>
#include <helpers.hpp>
#include <KitPidHelpers.hpp>
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> _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<SocketPoll>("HttpWsPoll"))
{
#if ENABLE_SSL
Poco::Net::initializeSSL();
// Just accept the certificate anyway for testing purposes
Poco::SharedPtr<Poco::Net::InvalidCertificateHandler> 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, invalidCertHandler, 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<http::WebSocketSession> 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<http::WebSocketSession> socket1
= loadDocAndGetSession(_socketPoll, _uri, documentURL, testname + "1 ");
std::shared_ptr<http::WebSocketSession> 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<http::WebSocketSession> 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<http::WebSocketSession> 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<http::WebSocketSession> socket1
= loadDocAndGetSession(_socketPoll, _uri, documentURL, "inactiveClient-1 ");
// Connect another and go inactive.
TST_LOG_NAME("inactiveClient-2 ", "Connecting second client.");
std::shared_ptr<http::WebSocketSession> 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 == "rulerupdate:" ||
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<http::WebSocketSession> socket0
= connectLOKit(_socketPoll, _uri, docURL, testname + "0 ");
std::shared_ptr<http::WebSocketSession> 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<Poco::JSON::Array::Ptr>();
LOK_ASSERT_EQUAL(static_cast<size_t>(1), array->size());
Poco::JSON::Object::Ptr viewInfoObj0 = array->getObject(0);
int viewid0 = viewInfoObj0->get("id").convert<int>();
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<Poco::JSON::Array::Ptr>();
LOK_ASSERT_EQUAL(static_cast<size_t>(2), array->size());
viewInfoObj0 = array->getObject(0);
Poco::JSON::Object::Ptr viewInfoObj1 = array->getObject(1);
viewid0 = viewInfoObj0->get("id").convert<int>();
int viewid1 = viewInfoObj1->get("id").convert<int>();
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<http::WebSocketSession> socket0
= connectLOKit(_socketPoll, _uri, docURL, testname + "0 ");
std::shared_ptr<http::WebSocketSession> 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<Poco::JSON::Object::Ptr>();
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<Poco::JSON::Object::Ptr>();
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: */