diff --git a/CMakeLists.txt b/CMakeLists.txt index d3efaf289..918672aa5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -164,6 +164,7 @@ endif() if(ANDROID) find_package(Sqlite3) + set(BUILD_TESTING FALSE) endif() ki18n_install(po) @@ -178,7 +179,7 @@ add_definitions(-DQT_NO_FOREACH) add_subdirectory(src) if (BUILD_TESTING) - find_package(Qt6 ${QT_MIN_VERSION} NO_MODULE COMPONENTS Test) + find_package(Qt6 ${QT_MIN_VERSION} NO_MODULE COMPONENTS Test HttpServer) add_subdirectory(autotests) # add_subdirectory(appiumtests) if (NOT ANDROID) diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index b1d12d5eb..1e7bd31cf 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -3,6 +3,10 @@ enable_testing() +add_library(neochat_server STATIC server.cpp) + +target_link_libraries(neochat_server PUBLIC Qt::HttpServer QuotientQt6) + add_definitions(-DDATA_DIR="${CMAKE_CURRENT_SOURCE_DIR}/data" ) ecm_add_test( @@ -85,6 +89,6 @@ ecm_add_test( ecm_add_test( actionstest.cpp - LINK_LIBRARIES neochat Qt::Test + LINK_LIBRARIES neochat Qt::Test neochat_server TEST_NAME actionstest ) diff --git a/autotests/actionstest.cpp b/autotests/actionstest.cpp index ec6f45fb4..9b9775bea 100644 --- a/autotests/actionstest.cpp +++ b/autotests/actionstest.cpp @@ -6,9 +6,11 @@ #include #include +#include "accountmanager.h" #include "chatbarcache.h" #include "models/actionsmodel.h" +#include "server.h" #include "testutils.h" using namespace Quotient; @@ -21,10 +23,12 @@ class ActionsTest : public QObject private: Connection *connection = nullptr; - TestUtils::TestRoom *room = nullptr; + NeoChatRoom *room = nullptr; void expectMessage(const QString &actionName, const QString &args, MessageType::Type type, const QString &message); + Server server; + private Q_SLOTS: void initTestCase(); void testActions(); @@ -34,8 +38,23 @@ private Q_SLOTS: void ActionsTest::initTestCase() { - connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org")); - room = new TestUtils::TestRoom(connection, QStringLiteral("#myroom:kde.org"), QLatin1String("test-min-sync.json")); + Connection::setRoomType(); + server.start(); + KLocalizedString::setApplicationDomain(QByteArrayLiteral("neochat")); + auto accountManager = new AccountManager(true); + QSignalSpy spy(accountManager, &AccountManager::connectionAdded); + connection = accountManager->accounts()->front(); + auto roomId = server.createRoom(u"@user:localhost:1234"_s); + server.inviteUser(roomId, u"@invited:example.com"_s); + server.banUser(roomId, u"@banned:example.com"_s); + server.joinUser(roomId, u"@example:example.com"_s); + + QSignalSpy syncSpy(connection, &Connection::syncDone); + // We need to wait for two syncs, as the next one won't have the changes yet + QVERIFY(syncSpy.wait()); + QVERIFY(syncSpy.wait()); + room = dynamic_cast(connection->room(roomId)); + QVERIFY(room); } void ActionsTest::testActions_data() @@ -90,7 +109,7 @@ static ActionsModel::Action findAction(const QString &name) void ActionsTest::expectMessage(const QString &actionName, const QString &args, MessageType::Type type, const QString &message) { auto action = findAction(actionName); - QSignalSpy spy(room, &TestUtils::TestRoom::showMessage); + QSignalSpy spy(room, &NeoChatRoom::showMessage); auto result = action.handle(args, room, nullptr); auto expected = QVariantList {type, message}; auto signal = spy.takeFirst(); @@ -106,14 +125,26 @@ void ActionsTest::testInvite() QCOMPARE(room->memberState(u"@banned:example.com"_s), Membership::Ban); expectMessage(u"invite"_s, connection->userId(), MessageType::Positive, u"You are already in this room."_s); QCOMPARE(room->memberState(connection->userId()), Membership::Join); - expectMessage(u"invite"_s, u"@example:example.org"_s, MessageType::Information, u"@example:example.org is already in this room."_s); - QCOMPARE(room->memberState(u"@example:example.org"_s), Membership::Join); + expectMessage(u"invite"_s, u"@example:example.com"_s, MessageType::Information, u"@example:example.com is already in this room."_s); + QCOMPARE(room->memberState(u"@example:example.com"_s), Membership::Join); QCOMPARE(room->memberState(u"@user:example.com"_s), Membership::Leave); expectMessage(u"invite"_s, u"@user:example.com"_s, MessageType::Positive, u"@user:example.com was invited into this room."_s); - //TODO mock server, wait for invite state to change - //TODO QCOMPARE(room->memberState(u"@user:example.com"_s), Membership::Invite); + QSignalSpy spy(room, &NeoChatRoom::changed); + QVERIFY(spy.wait()); + + auto tries = 0; + + while (room->memberState(u"@user:example.com"_s) != Membership::Invite) { + QVERIFY(spy.wait()); + tries += 1; + if (tries > 3) { + QVERIFY(false); + } + } + + QCOMPARE(room->memberState(u"@user:example.com"_s), Membership::Invite); } QTEST_MAIN(ActionsTest) diff --git a/autotests/data/localhost.crt b/autotests/data/localhost.crt new file mode 100644 index 000000000..35309c919 --- /dev/null +++ b/autotests/data/localhost.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDNTCCAh2gAwIBAgIUXbyWfTfcvVLrVB1qx36pW/7IkwMwDQYJKoZIhvcNAQEL +BQAwQjELMAkGA1UEBhMCWFgxFTATBgNVBAcMDERlZmF1bHQgQ2l0eTEcMBoGA1UE +CgwTRGVmYXVsdCBDb21wYW55IEx0ZDAgFw0yNDEyMjQxNTAxMDNaGA8yNTcyMDcy +NDE1MDEwM1owQjELMAkGA1UEBhMCWFgxFTATBgNVBAcMDERlZmF1bHQgQ2l0eTEc +MBoGA1UECgwTRGVmYXVsdCBDb21wYW55IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBAKlxZ540TQ1uUDAR7ZJ9ue0PzcD2dPmblIIddyekvZS59V7X +drhamclXpHE2EelR87Sexst0BaHH/jmrHwxCtwbeXHZ8ueJHkGHJ5DLZCCiwfG+Q +gml7wlSXxXz37vie2tdlZh2yJSM8yvLAYceHb2zOskaGvul7ZITIS0JrPc3o6VZk ++MYGkYtA2JfUsv3jH4oQbxOf7RXqhWNAXbB+3hlwRBwMIdyoBNK6YS9QSrTeS9jj +UqgO5QmaQZOVvpaPf1Y/rHHLd2Qa6+a/cCJ1sr2biagb75AihpQFsK/oy6D1PP70 +zTe7hPWn/efEpmtCV7CQ8ti4cRu0Kjy0T8grtCsCAwEAAaMhMB8wHQYDVR0OBBYE +FIFlylzwADNLfgTDNkhFeFelaEDxMA0GCSqGSIb3DQEBCwUAA4IBAQBQ2rw4GLIU +v+GY7Qru9LttkrQPd2bZXKxDMd/jT+wjmMVtqS4MAsCuDYwaYLjU1aWyqy0mN+lY +A17kD0VjBNBy45sYqkZveY0ks8mCScBemtrIDmjz2tiueecBIEASwEPBOZgv5/MV +cz864FiChF+2r8Zl8bhycGy9DEpRjzYKvIQWSDHQ3zpuh3iBnjfoieLHWX2kKCpk +ouS3V6485rHNCWsZT5IcCwfBFQkOuWRJpIazpz4AfwZh1TK9+bgiKA5EyZjSNrKw +xGQSpMSTRQMB0/FOCL/AixhN9unVFUViqUcdtSfoHE1VyBHv9kDT/cYms/Xl4B0t +/ZSQJ/D/Km1+ +-----END CERTIFICATE----- diff --git a/autotests/data/localhost.key b/autotests/data/localhost.key new file mode 100644 index 000000000..a60535528 --- /dev/null +++ b/autotests/data/localhost.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCpcWeeNE0NblAw +Ee2SfbntD83A9nT5m5SCHXcnpL2UufVe13a4WpnJV6RxNhHpUfO0nsbLdAWhx/45 +qx8MQrcG3lx2fLniR5BhyeQy2QgosHxvkIJpe8JUl8V89+74ntrXZWYdsiUjPMry +wGHHh29szrJGhr7pe2SEyEtCaz3N6OlWZPjGBpGLQNiX1LL94x+KEG8Tn+0V6oVj +QF2wft4ZcEQcDCHcqATSumEvUEq03kvY41KoDuUJmkGTlb6Wj39WP6xxy3dkGuvm +v3AidbK9m4moG++QIoaUBbCv6Mug9Tz+9M03u4T1p/3nxKZrQlewkPLYuHEbtCo8 +tE/IK7QrAgMBAAECggEAH9qmeKrra2F4KLlOGNKS//qPGz4Z+ozhi95/NpA1Zb7Z +3pUSCBFcROo5i2D3WA4kiymoRLpQjrv60puVcCggoWVvK4VCKsR6Y6/hOx/q9T9M +fWrE4ZC3FVEc+uPfZJT0nja9TkrdyXSV0LITD8Ap1eI7yJ9vR5R/bqj64QcpLMrU +QeoQIy1oTMR+qdjj33duyRwBZU3Yf8FRB2iW6OILZ8hzFo1jngec7dph9a1RK4e0 +mEPdc9ywsKlDM7P0Y7zdmjar5XtQn87GiwNhz23f1fzCC2axLtOW0Xm4e4Qumehb +WrIi6Vfq8IWMglU7QrBJ7iR0Ls+XoKA5GxomV2IJZQKBgQDoIkOl5YGPQ3iGR+WK +e5/2Ml4G/uURzYiOlzSsyfoPXyO4EI2BJd5HkH+EvfgRx4xKkxUZRJdzR7llYPl8 +BFYcFitvhO8SbD0mNAB5YW7f+3v1pgEN2umzoKd389Zx5WqTZ7YB1VG5RN/Q1JJL +2JM0Xgamq2vNtx3roRPxDBeW7QKBgQC63R/bmACJbgIzfaVBX4Zie3NQG0/Hf+gF +LnBwUmQDZOR7MY+kSiIUVMn3NuZRiCSCFBVwApruyK8r535JCibTVm5PWjvhFddY +LgaPOCKGlm9TLScjoH1pErYgG3uJ4nXeRfXhg4mco6EkrC7RzQywrd0VDoqpuc1Y +EKfEsYk8dwKBgE+mSh3nNOBKX1V73+f3aTiZqaeu2DyWkG+UtE9BclrJ40Cp9VPG +AZH+o7KRWEgJdzqzYv7riSfWCWgesRv7hOxYMwktzLY+i3DLUQpVAy05ZhwwnJX7 +ckrfKfc/pGoqNLplUI8qecMfPciy14vMwR2r0Y5orTHFzi9mcqg35PQ1AoGAW2LX +OLq+0HdHhk0Va8I+450CSRQCUUvhed87SANTPEG0Z/dWC3/h6NWKrGdh/k+5oxAV +Z+EuSkdFPBCLt0bKtCKZ8h7sF+lplotz08kdQXsC2MfFU2wiySdIgK1QHp/tCxZl +6LM+sqdnoJrAjwRcB3AQJkMlV1ox7ba/hbdZqYMCgYBS6+JUXSSASpm5ZHd32a8m +xwryEZ7H6Hek6lvMHdxmwoKat5dCavxw64nrtyeeGZpg1W3zLLyamF9x/8kMyr6y +KKvtBfJ5sCvAbt80o9Pbs6R3yDB3AKiD3s3PQK7lol1nhE/8IbsF2r8JEQVcYd/k +oBzkl7MrMyLhhaCqSxwqQQ== +-----END PRIVATE KEY----- diff --git a/autotests/server.cpp b/autotests/server.cpp new file mode 100644 index 000000000..e9598a907 --- /dev/null +++ b/autotests/server.cpp @@ -0,0 +1,235 @@ +// SPDX-FileCopyrightText: 2025 Tobias Fella +// SPDX-License-Identifier: LGPL-2.0-or-later + +#include "server.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +using namespace Qt::Literals::StringLiterals; + +QString generateEventId() +{ + return u"$"_s + QString::fromLatin1(QCryptographicHash::hash(QUuid::createUuid().toString().toLatin1(), QCryptographicHash::Sha1).toBase64()); +} + +QString generateRoomId() +{ + return u"!%1:localhost:1234"_s + .arg(QString::fromLatin1(QCryptographicHash::hash(QUuid::createUuid().toString().toLatin1(), QCryptographicHash::Sha1).toBase64())) + .replace(u'/', QChar()); +} + +Server::Server() +{ +} + +void Server::start() +{ + QObject::connect(Quotient::NetworkAccessManager::instance(), + &QNetworkAccessManager::sslErrors, + Quotient::NetworkAccessManager::instance(), + [](QNetworkReply *reply) { + reply->ignoreSslErrors(); + }); + m_server.route(u"/.well-known/matrix/client"_s, QHttpServerRequest::Method::Get, [](QHttpServerResponder &responder) { + responder.write(QJsonDocument(QJsonObject{ + {u"m.homeserver"_s, QJsonObject{{u"base_url"_s, u"https://localhost:1234"_s}}}, + }), + QHttpServerResponder::StatusCode::Ok); + }); + m_server.route(u"/_matrix/client/versions"_s, QHttpServerRequest::Method::Get, [](QHttpServerResponder &responder) { + responder.write(QJsonDocument(QJsonObject{ + {u"versions"_s, + QJsonArray{ + u"v1.0"_s, + u"v1.1"_s, + u"v1.2"_s, + u"v1.3"_s, + u"v1.4"_s, + u"v1.5"_s, + u"v1.6"_s, + u"v1.7"_s, + u"v1.8"_s, + u"v1.9"_s, + u"v1.10"_s, + u"v1.11"_s, + u"v1.12"_s, + u"v1.13"_s, + }}, + }), + QHttpServerResponder::StatusCode::Ok); + }); + m_server.route(u"/_matrix/client/v3/capabilities"_s, QHttpServerRequest::Method::Get, [](QHttpServerResponder &responder) { + responder.write( + QJsonDocument(QJsonObject{{u"capabilities"_s, + QJsonObject{ + {u"m.room_versions"_s, QJsonObject{{u"m.available"_s, QJsonObject{{u"1"_s, u"stable"_s}}}, {u"default"_s, u"1"_s}}}, + }}}), + QHttpServerResponder::StatusCode::Ok); + }); + m_server.route(u"/_matrix/client/v3/account/whoami"_s, QHttpServerRequest::Method::Get, [](QHttpServerResponder &responder) { + responder.write(QJsonDocument(QJsonObject{ + {u"device_id"_s, u"device_id_1234"_s}, + {u"user_id"_s, u"@user:localhost:1234"_s}, + }), + QHttpServerResponder::StatusCode::Ok); + }); + + m_server.route(u"/_matrix/client/v3/login"_s, QHttpServerRequest::Method::Post, [](QHttpServerResponder &responder) { + // TODO + // if data["identifier"]["user"] != "user" or data["password"] != "1234": + // abort(403) + responder.write(QJsonDocument(QJsonObject{ + {u"access_token"_s, u"token_login"_s}, + {u"device_id"_s, u"device_1234"_s}, + {u"user_id"_s, u"@user:localhost:1234"_s}, + }), + QHttpServerResponder::StatusCode::Ok); + }); + + m_server.route(u"/_matrix/client/v3/login"_s, QHttpServerRequest::Method::Get, [](QHttpServerResponder &responder) { + responder.write(QJsonDocument(QJsonObject{ + {u"flows"_s, QJsonArray{QJsonObject{{u"type"_s, u"m.login.password"_s}}}}, + }), + QHttpServerResponder::StatusCode::Ok); + }); + + m_server.route(u"/_matrix/client/v3/rooms//invite"_s, + QHttpServerRequest::Method::Post, + [this](const QString &roomId, QHttpServerResponder &responder, const QHttpServerRequest &request) { + m_invitedUsers[roomId] += QJsonDocument::fromJson(request.body()).object()[u"user_id"_s].toString(); + responder.write(QJsonDocument(QJsonObject{}), QHttpServerResponder::StatusCode::Ok); + }); + + m_server.route(u"/_matrix/client/r0/sync"_s, QHttpServerRequest::Method::Get, [this](QHttpServerResponder &responder) { + QMap stateEvents; + + for (const auto &[roomId, matrixId] : m_roomsToCreate) { + stateEvents[roomId] += QJsonObject{ + {u"content"_s, QJsonObject{{u"room_version"_s, u"11"_s}}}, + {u"event_id"_s, generateEventId()}, + {u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()}, + {u"room_id"_s, roomId}, + {u"sender"_s, matrixId}, + {u"state_key"_s, QString()}, + {u"type"_s, u"m.room.create"_s}, + {u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}}, + }; + stateEvents[roomId] += QJsonObject{ + {u"content"_s, QJsonObject{{u"displayname"_s, u"User"_s}, {u"membership"_s, u"join"_s}}}, + {u"event_id"_s, generateEventId()}, + {u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()}, + {u"room_id"_s, roomId}, + {u"sender"_s, matrixId}, + {u"state_key"_s, matrixId}, + {u"type"_s, u"m.room.member"_s}, + {u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}}, + }; + } + m_roomsToCreate.clear(); + for (const auto &roomId : m_invitedUsers.keys()) { + const auto &values = m_invitedUsers[roomId]; + for (const auto &value : values) { + stateEvents[roomId] += QJsonObject{ + {u"content"_s, QJsonObject{{u"displayname"_s, u"User"_s}, {u"membership"_s, u"invite"_s}}}, + {u"event_id"_s, generateEventId()}, + {u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()}, + {u"room_id"_s, roomId}, + {u"sender"_s, u"@user:localhost:1234"_s}, + {u"state_key"_s, value}, + {u"type"_s, u"m.room.member"_s}, + {u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}}, + }; + } + } + m_invitedUsers.clear(); + + for (const auto &roomId : m_bannedUsers.keys()) { + const auto &values = m_bannedUsers[roomId]; + for (const auto &value : values) { + stateEvents[roomId] += QJsonObject{ + {u"content"_s, QJsonObject{{u"displayname"_s, u"User"_s}, {u"membership"_s, u"ban"_s}}}, + {u"event_id"_s, generateEventId()}, + {u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()}, + {u"room_id"_s, roomId}, + {u"sender"_s, u"@user:localhost:1234"_s}, + {u"state_key"_s, value}, + {u"type"_s, u"m.room.member"_s}, + {u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}}, + }; + } + } + m_bannedUsers.clear(); + + for (const auto &roomId : m_joinedUsers.keys()) { + const auto &values = m_joinedUsers[roomId]; + for (const auto &value : values) { + stateEvents[roomId] += QJsonObject{ + {u"content"_s, QJsonObject{{u"displayname"_s, u"User"_s}, {u"membership"_s, u"join"_s}}}, + {u"event_id"_s, generateEventId()}, + {u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()}, + {u"room_id"_s, roomId}, + {u"sender"_s, u"@user:localhost:1234"_s}, + {u"state_key"_s, value}, + {u"type"_s, u"m.room.member"_s}, + {u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}}, + }; + } + } + m_joinedUsers.clear(); + + QJsonObject rooms; + for (const auto &roomId : stateEvents.keys()) { + rooms[roomId] = QJsonObject{{u"state"_s, QJsonObject{{u"events"_s, stateEvents[roomId]}}}}; + } + + responder.write(QJsonDocument(QJsonObject{{u"rooms"_s, QJsonObject{{u"join"_s, rooms}}}}), QHttpServerResponder::StatusCode::Ok); + }); + + QSslConfiguration config; + QFile key(QStringLiteral(DATA_DIR) + u"/localhost.key"_s); + key.open(QFile::ReadOnly); + config.setPrivateKey(QSslKey(&key, QSsl::Rsa)); + config.setLocalCertificate(QSslCertificate::fromPath(QStringLiteral(DATA_DIR) + u"/localhost.crt"_s).front()); + m_sslServer.setSslConfiguration(config); + if (!m_sslServer.listen(QHostAddress::LocalHost, 1234) || !m_server.bind(&m_sslServer)) { + qFatal() << "Server failed to listen on a port."; + return; + } else { + qWarning() << "Server listening"; + } +} + +QString Server::createRoom(const QString &matrixId) +{ + auto roomId = generateRoomId(); + m_roomsToCreate += {roomId, matrixId}; + return roomId; +} + +void Server::inviteUser(const QString &roomId, const QString &matrixId) +{ + m_invitedUsers[roomId] += matrixId; +} + +void Server::banUser(const QString &roomId, const QString &matrixId) +{ + m_bannedUsers[roomId] += matrixId; +} + +void Server::joinUser(const QString &roomId, const QString &matrixId) +{ + m_joinedUsers[roomId] += matrixId; +} diff --git a/autotests/server.h b/autotests/server.h new file mode 100644 index 000000000..c9a4e1d6c --- /dev/null +++ b/autotests/server.h @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2025 Tobias Fella +// SPDX-License-Identifier: LGPL-2.0-or-later + +#include +#include + +class Server +{ +public: + Server(); + + void start(); + + /** + * Create a room and place the user with id matrixId in it. + * Returns the room's id + */ + QString createRoom(const QString &matrixId); + + void inviteUser(const QString &roomId, const QString &matrixId); + void banUser(const QString &roomId, const QString &matrixId); + void joinUser(const QString &roomId, const QString &matrixId); + +private: + QHttpServer m_server; + QSslServer m_sslServer; + + QHash> m_invitedUsers; + QHash> m_bannedUsers; + QHash> m_joinedUsers; + + QList> m_roomsToCreate; +}; diff --git a/src/libneochat/accountmanager.cpp b/src/libneochat/accountmanager.cpp index 97db00586..763f7a504 100644 --- a/src/libneochat/accountmanager.cpp +++ b/src/libneochat/accountmanager.cpp @@ -23,12 +23,10 @@ AccountManager::AccountManager(bool testMode, QObject *parent) loadAccountsFromCache(); }); } else { - auto c = new NeoChatConnection(this); + auto c = new NeoChatConnection(QUrl(u"https://localhost:1234"_s), this); c->assumeIdentity(u"@user:localhost:1234"_s, u"device_1234"_s, u"token_1234"_s); - connect(c, &NeoChatConnection::connected, this, [c, this]() { - m_accountRegistry->add(c); - c->syncLoop(); - }); + m_accountRegistry->add(c); + c->syncLoop(); } }