diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index 1e7bd31cf..9297a4d22 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -92,3 +92,9 @@ ecm_add_test( LINK_LIBRARIES neochat Qt::Test neochat_server TEST_NAME actionstest ) + +ecm_add_test( + roommanagertest.cpp + LINK_LIBRARIES neochat Qt::Test neochat_server + TEST_NAME roommanagertest +) diff --git a/autotests/roommanagertest.cpp b/autotests/roommanagertest.cpp new file mode 100644 index 000000000..f2962967a --- /dev/null +++ b/autotests/roommanagertest.cpp @@ -0,0 +1,132 @@ +// SPDX-FileCopyrightText: 2024 Tobias Fella +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +#include +#include +#include +#include + +#include "accountmanager.h" +#include "models/actionsmodel.h" +#include "roommanager.h" + +#include "server.h" +#include "testutils.h" + +using namespace Quotient; + +class RoomManagerTest : public QObject +{ + Q_OBJECT + +private: + NeoChatConnection *connection = nullptr; + NeoChatRoom *room = nullptr; + + Server server; + +private Q_SLOTS: + void initTestCase(); + void testMaximizeMedia(); +}; + +void RoomManagerTest::initTestCase() +{ + Connection::setRoomType(); + server.start(); + KLocalizedString::setApplicationDomain(QByteArrayLiteral("neochat")); + auto accountManager = new AccountManager(true); + QSignalSpy spy(accountManager, &AccountManager::connectionAdded); + connection = dynamic_cast(accountManager->accounts()->front()); + QVERIFY(connection); + auto roomId = server.createRoom(u"@user:localhost:1234"_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); + RoomManager::instance().setConnection(connection); + QSignalSpy roomSpy(&RoomManager::instance(), &RoomManager::currentRoomChanged); + RoomManager::instance().resolveResource(room->id()); + QVERIFY(roomSpy.size() > 0); +} + +void RoomManagerTest::testMaximizeMedia() +{ + QSignalSpy spy(&RoomManager::instance(), &RoomManager::showMaximizedMedia); + QSignalSpy syncSpy(connection, &Connection::syncDone); + + QTest::ignoreMessage(QtMsgType::QtWarningMsg, "Tried to open media for empty event id"); + RoomManager::instance().maximizeMedia(QString()); + QVERIFY(!spy.wait(10)); + + QTest::ignoreMessage(QtMsgType::QtWarningMsg, "Tried to open media for unknown event id \"Doesn't exist\""); + RoomManager::instance().maximizeMedia(u"Doesn't exist"_s); + QVERIFY(!spy.wait(10)); + + const auto eventWithoutMedia = server.sendEvent(room->id(), + u"m.room.message"_s, + QJsonObject({ + {u"body"_s, u"Foo"_s}, + {u"format"_s, u"org.matrix.custom.html"_s}, + {u"formatted_body"_s, u"Foo"_s}, + {u"msgtype"_s, u"m.text"_s}, + })); + QVERIFY(syncSpy.wait()); + QVERIFY(syncSpy.wait()); + QTest::ignoreMessage(QtMsgType::QtWarningMsg, u"Tried to open media for unknown event id \"%1\""_s.arg(eventWithoutMedia).toLatin1().data()); + RoomManager::instance().maximizeMedia(eventWithoutMedia); + QVERIFY(!spy.wait(10)); + + // NOTE: This is supposed to test that maximizing pending media works correctly. This probably doesn't work in the UI yet, but at least the backend supports + // it. If the server ever learns how to process events, this becomes pointless and we need to find a way of preventing *these* events from arriving + auto pendingEventWithoutMedia = room->postText(u"Hello"_s); + QTest::ignoreMessage(QtMsgType::QtWarningMsg, u"Tried to open media for unknown event id \"%1\""_s.arg(pendingEventWithoutMedia).toLatin1().data()); + RoomManager::instance().maximizeMedia(pendingEventWithoutMedia); + QVERIFY(!spy.wait(10)); + + const auto eventWithMedia = server.sendEvent(room->id(), + u"m.room.message"_s, + QJsonObject({ + {u"body"_s, u"Foo"_s}, + {u"filename"_s, u"foo.jpg"_s}, + {u"info"_s, + QJsonObject{ + {u"h"_s, 1000}, + {u"w"_s, 2000}, + {u"size"_s, 10000}, + {u"mimetype"_s, u"image/png"_s}, + }}, + {u"msgtype"_s, u"m.image"_s}, + {u"url"_s, u"mxc://foo.bar/asdf"_s}, + })); + QVERIFY(syncSpy.wait()); + QVERIFY(syncSpy.wait()); + QVERIFY(syncSpy.wait()); + RoomManager::instance().maximizeMedia(eventWithMedia); + QVERIFY(spy.size() == 1); + QVERIFY(spy[0][0] == 0); + + auto pendingEventWithMedia = room->postJson(u"m.room.message"_s, + QJsonObject({ + {u"body"_s, u"Foo"_s}, + {u"filename"_s, u"foo.jpg"_s}, + {u"info"_s, + QJsonObject{ + {u"h"_s, 1000}, + {u"w"_s, 2000}, + {u"size"_s, 10000}, + {u"mimetype"_s, u"image/png"_s}, + }}, + {u"msgtype"_s, u"m.image"_s}, + {u"url"_s, u"mxc://foo.bar/asdf"_s}, + })); + RoomManager::instance().maximizeMedia(pendingEventWithMedia); + QVERIFY(spy.size() == 2); + QVERIFY(spy[1][0] == 0); +} + +QTEST_MAIN(RoomManagerTest) +#include "roommanagertest.moc" diff --git a/autotests/server.cpp b/autotests/server.cpp index e9598a907..c48c6e145 100644 --- a/autotests/server.cpp +++ b/autotests/server.cpp @@ -115,28 +115,36 @@ void Server::start() m_server.route(u"/_matrix/client/r0/sync"_s, QHttpServerRequest::Method::Get, [this](QHttpServerResponder &responder) { QMap stateEvents; + QMap roomAccountData; - for (const auto &[roomId, matrixId] : m_roomsToCreate) { - stateEvents[roomId] += QJsonObject{ + for (const auto &roomData : m_roomsToCreate) { + stateEvents[roomData.id] += 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"room_id"_s, roomData.id}, + {u"sender"_s, roomData.members[0]}, {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}}}, - }; + for (const auto &member : roomData.members) { + stateEvents[roomData.id] += 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, roomData.id}, + {u"sender"_s, member}, + {u"state_key"_s, member}, + {u"type"_s, u"m.room.member"_s}, + {u"unsigned"_s, QJsonObject{{u"age"_s, 1234}}}, + }; + } + QJsonObject tags; + for (const auto &tag : roomData.tags) { + tags[tag] = QJsonObject(); + } + roomAccountData[roomData.id] += QJsonObject{{u"type"_s, u"m.tag"_s}, {u"content"_s, QJsonObject{{u"tags"_s, tags}}}}; } m_roomsToCreate.clear(); for (const auto &roomId : m_invitedUsers.keys()) { @@ -191,11 +199,18 @@ void Server::start() m_joinedUsers.clear(); QJsonObject rooms; - for (const auto &roomId : stateEvents.keys()) { - rooms[roomId] = QJsonObject{{u"state"_s, QJsonObject{{u"events"_s, stateEvents[roomId]}}}}; + auto keys = stateEvents.keys() + m_events.keys(); + for (const auto &roomId : QSet(keys.begin(), keys.end())) { + rooms[roomId] = QJsonObject{ + {u"state"_s, QJsonObject{{u"events"_s, stateEvents[roomId]}}}, + {u"account_data"_s, QJsonObject{{u"events"_s, roomAccountData[roomId]}}}, + {u"timeline"_s, QJsonObject{{u"events"_s, m_events[roomId]}}}, + }; } + m_events.clear(); - responder.write(QJsonDocument(QJsonObject{{u"rooms"_s, QJsonObject{{u"join"_s, rooms}}}}), QHttpServerResponder::StatusCode::Ok); + auto json = QJsonObject{{u"rooms"_s, QJsonObject{{u"join"_s, rooms}}}}; + responder.write(QJsonDocument(json), QHttpServerResponder::StatusCode::Ok); }); QSslConfiguration config; @@ -215,7 +230,11 @@ void Server::start() QString Server::createRoom(const QString &matrixId) { auto roomId = generateRoomId(); - m_roomsToCreate += {roomId, matrixId}; + m_roomsToCreate += RoomData{ + .members = {matrixId}, + .id = roomId, + .tags = {}, + }; return roomId; } @@ -233,3 +252,23 @@ void Server::joinUser(const QString &roomId, const QString &matrixId) { m_joinedUsers[roomId] += matrixId; } + +QString Server::createServerNoticesRoom(const QString &matrixId) +{ + auto roomId = createRoom(matrixId); + m_roomsToCreate.last().tags = {u"m.server_notice"_s}; + return roomId; +} + +QString Server::sendEvent(const QString &roomId, const QString &eventType, const QJsonObject &content) +{ + const auto eventId = generateEventId(); + m_events[roomId] += QJsonObject{ + {u"type"_s, eventType}, + {u"content"_s, content}, + {u"sender"_s, u"@foo:server.com"_s}, + {u"event_id"_s, eventId}, + {u"origin_server_ts"_s, QDateTime::currentMSecsSinceEpoch()}, + }; + return eventId; +} diff --git a/autotests/server.h b/autotests/server.h index c9a4e1d6c..4917b80fb 100644 --- a/autotests/server.h +++ b/autotests/server.h @@ -4,6 +4,12 @@ #include #include +struct RoomData { + QStringList members; + QString id; + QStringList tags; +}; + class Server { public: @@ -21,6 +27,12 @@ public: void banUser(const QString &roomId, const QString &matrixId); void joinUser(const QString &roomId, const QString &matrixId); + /** + * Create a server notices room. + */ + QString createServerNoticesRoom(const QString &matrixId); + QString sendEvent(const QString &roomId, const QString &eventType, const QJsonObject &content); + private: QHttpServer m_server; QSslServer m_sslServer; @@ -29,5 +41,6 @@ private: QHash> m_bannedUsers; QHash> m_joinedUsers; - QList> m_roomsToCreate; + QList m_roomsToCreate; + QMap m_events; }; diff --git a/src/app/roommanager.cpp b/src/app/roommanager.cpp index 9d62c9f51..b42a63d4a 100644 --- a/src/app/roommanager.cpp +++ b/src/app/roommanager.cpp @@ -245,6 +245,7 @@ void RoomManager::maximizeMedia(const QString &eventId) const auto index = m_mediaMessageFilterModel->getRowForEventId(eventId); if (index == -1) { + qWarning() << "Tried to open media for unknown event id" << eventId; return; }