// SPDX-FileCopyrightText: 2023 James Graham // SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL #include #include #include "texthandler.h" #include #include #include using namespace Quotient; class TestRoom : public NeoChatRoom { public: using NeoChatRoom::NeoChatRoom; void update(SyncRoomData &&data, bool fromCache = false) { Room::updateData(std::move(data), fromCache); } }; class TextHandlerTest : public QObject { Q_OBJECT private: Connection *connection = nullptr; TestRoom *room = nullptr; private Q_SLOTS: void initTestCase(); void allowedAttributes(); void stripDisallowedTags(); void stripDisallowedAttributes(); void emptyCodeTags(); void sendSimpleStringCase(); void sendSingleParaMarkup(); void sendMultipleSectionMarkup(); void sendBadLinks(); void sendEscapeCode(); void sendCodeClass(); void receiveStripReply(); void receivePlainTextIn(); void receiveRichInPlainOut_data(); void receiveRichInPlainOut(); void receivePlainStripHtml(); void receivePlainStripMarkup(); void receiveStripNewlines(); void receiveRichUserPill(); void receiveRichStrikethrough(); void receiveRichtextIn(); void receiveRichMxcUrl(); void receiveRichPlainUrl(); void receiveRichEmote(); void receiveRichEdited_data(); void receiveRichEdited(); void receiveLineSeparator(); }; #ifdef QUOTIENT_07 void TextHandlerTest::initTestCase() { connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org")); room = new TestRoom(connection, QStringLiteral("#myroom:kde.org"), JoinState::Join); const auto json = QJsonDocument::fromJson(R"EVENT({ "account_data": { "events": [ { "content": { "tags": { "u.work": { "order": 0.9 } } }, "type": "m.tag" }, { "content": { "custom_config_key": "custom_config_value" }, "type": "org.example.custom.room.config" } ] }, "ephemeral": { "events": [ { "content": { "user_ids": [ "@alice:matrix.org", "@bob:example.com" ] }, "room_id": "!jEsUZKDJdhlrceRyVU:example.org", "type": "m.typing" } ] }, "state": { "events": [ { "content": { "avatar_url": "mxc://example.org/SEsfnsuifSDFSSEF", "displayname": "Alice Margatroid", "membership": "join", "reason": "Looking for support" }, "event_id": "$143273582443PhrSn:example.org", "origin_server_ts": 1432735824653, "room_id": "!jEsUZKDJdhlrceRyVU:example.org", "sender": "@example:example.org", "state_key": "@alice:example.org", "type": "m.room.member", "unsigned": { "age": 1234 } } ] }, "summary": { "m.heroes": [ "@alice:example.com", "@bob:example.com" ], "m.invited_member_count": 0, "m.joined_member_count": 2 }, "timeline": { "events": [ { "content": { "body": "This is an **example** text message", "format": "org.matrix.custom.html", "formatted_body": "This is an example text message", "msgtype": "m.text" }, "event_id": "$143273582443PhrSn:example.org", "origin_server_ts": 1432735824654, "room_id": "!jEsUZKDJdhlrceRyVU:example.org", "sender": "@example:example.org", "type": "m.room.message", "unsigned": { "age": 1232 } }, { "content": { "body": "/me This is an emote.", "format": "org.matrix.custom.html", "formatted_body": "This is an emote.", "msgtype": "m.emote" }, "event_id": "$153273582443PhrSn:example.org", "origin_server_ts": 1532735824654, "room_id": "!jEsUZKDJdhlrceRyVU:example.org", "sender": "@example:example.org", "type": "m.room.message", "unsigned": { "age": 1231 } }, { "content": { "body": "tested", "msgtype": "m.text" }, "event_id": "$zrCiBxBnqqTn0Z5FY78qSZAszno_w8nJJXzfBULG-3E", "origin_server_ts": 1680948575928, "room_id": "!jEsUZKDJdhlrceRyVU:example.org", "sender": "@example:example.org", "type": "m.room.message", "unsigned": { "age": 1747776, "m.relations": { "m.replace": { "event_id": "$UX0PlpyI7vYO32iHMuuYEP7ECMh4sX3XLGiB2SwM4mQ", "origin_server_ts": 1680948580992, "sender": "@example:example.org" } } } } ], "limited": true, "prev_batch": "t34-23535_0_0" } })EVENT"); SyncRoomData roomData(QStringLiteral("@bob:kde.org"), JoinState::Join, json.object()); room->update(std::move(roomData)); } #endif void TextHandlerTest::allowedAttributes() { const QString testInputString1 = QStringLiteral("

Test

"); const QString testOutputString1 = QStringLiteral("

Test

"); // Handle urls where the href has either single (') or double (") quotes. const QString testInputString2 = QStringLiteral("

linklink

"); const QString testOutputString2 = QStringLiteral("

linklink

"); TextHandler testTextHandler; testTextHandler.setData(testInputString1); QCOMPARE(testTextHandler.handleSendText(), testOutputString1); QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString1); testTextHandler.setData(testInputString2); QCOMPARE(testTextHandler.handleSendText(), testOutputString2); QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString2); } void TextHandlerTest::stripDisallowedTags() { const QString testInputString = QStringLiteral("

Allowed

Allowed Disallowed"); const QString testOutputString = QStringLiteral("

Allowed

Allowed Disallowed"); TextHandler testTextHandler; testTextHandler.setData(testInputString); QCOMPARE(testTextHandler.handleSendText(), testOutputString); QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString); } void TextHandlerTest::stripDisallowedAttributes() { const QString testInputString = QStringLiteral("

Test

"); const QString testOutputString = QStringLiteral("

Test

"); TextHandler testTextHandler; testTextHandler.setData(testInputString); QCOMPARE(testTextHandler.handleSendText(), testOutputString); QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString); } /** * Make sure that empty code tags are handled. * (this was a bug during development hence the test) */ void TextHandlerTest::emptyCodeTags() { const QString testInputString = QStringLiteral("
"); const QString testOutputString = QStringLiteral("
"); TextHandler testTextHandler; testTextHandler.setData(testInputString); QCOMPARE(testTextHandler.handleSendText(), testOutputString); QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString); } void TextHandlerTest::sendSimpleStringCase() { const QString testInputString = QStringLiteral("This data should just be put in a paragraph."); const QString testOutputString = QStringLiteral("

This data should just be put in a paragraph.

"); TextHandler testTextHandler; testTextHandler.setData(testInputString); QCOMPARE(testTextHandler.handleSendText(), testOutputString); } void TextHandlerTest::sendSingleParaMarkup() { const QString testInputString = QStringLiteral( "Text para with **bold**, *italic*, [link](https://kde.org), ![image](mxc://kde.org/aebd3ffd40503e1ef0525bf8f0d60282fec6183e), `inline code`."); const QString testOutputString = QStringLiteral( "

Text para with bold, italic, link, \"image\", inline code.

"); TextHandler testTextHandler; testTextHandler.setData(testInputString); QCOMPARE(testTextHandler.handleSendText(), testOutputString); } void TextHandlerTest::sendMultipleSectionMarkup() { const QString testInputString = QStringLiteral("Text para\n> blockquote\n* List 1\n* List 2\n1. one\n2. two\n# Heading 1\n## Heading 2\nhorizontal rule\n\n---\n```\ncodeblock\n```"); const QString testOutputString = QStringLiteral( "

Text para

\n
\n

blockquote

\n
\n
    \n
  • List 1
  • \n
  • List " "2
  • \n
\n
    \n
  1. one
  2. \n
  3. two
  4. \n
\n

Heading 1

\n

Heading 2

\n

horizontal " "rule

\n
\n
codeblock\n
"); TextHandler testTextHandler; testTextHandler.setData(testInputString); QCOMPARE(testTextHandler.handleSendText(), testOutputString); } void TextHandlerTest::sendBadLinks() { const QString testInputString = QStringLiteral("[link](kde.org), ![image](https://kde.org/aebd3ffd40503e1ef0525bf8f0d60282fec6183e)"); const QString testOutputString = QStringLiteral("

link, \"image\"

"); TextHandler testTextHandler; testTextHandler.setData(testInputString); QCOMPARE(testTextHandler.handleSendText(), testOutputString); } /** * All text between code tags is treated as plain so it should get escaped. */ void TextHandlerTest::sendEscapeCode() { const QString testInputString = QStringLiteral("```\n

Test some code

\n```"); const QString testOutputString = QStringLiteral("
<p>Test <span style="font-size:50px;">some</span> code</p>\n
"); TextHandler testTextHandler; testTextHandler.setData(testInputString); QCOMPARE(testTextHandler.handleSendText(), testOutputString); } void TextHandlerTest::sendCodeClass() { const QString testInputString = QStringLiteral("```html\nsome code\n```\n
some more code
"); const QString testOutputString = QStringLiteral("
some code\n
\n
some more code
"); TextHandler testTextHandler; testTextHandler.setData(testInputString); QCOMPARE(testTextHandler.handleSendText(), testOutputString); } void TextHandlerTest::receiveStripReply() { const QString testInputString = QStringLiteral( "
In reply to@alice:example.org
Message replied to.
Reply message."); const QString testOutputString = QStringLiteral("Reply message."); TextHandler testTextHandler; testTextHandler.setData(testInputString); QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString); QCOMPARE(testTextHandler.handleRecievePlainText(), testOutputString); } void TextHandlerTest::receiveRichInPlainOut_data() { QTest::addColumn("testInputString"); QTest::addColumn("testOutputString"); QTest::newRow("ampersand") << QStringLiteral("a & b") << QStringLiteral("a & b"); QTest::newRow("quote") << QStringLiteral(""a and b"") << QStringLiteral("\"a and b\""); } void TextHandlerTest::receiveRichInPlainOut() { QFETCH(QString, testInputString); QFETCH(QString, testOutputString); TextHandler testTextHandler; testTextHandler.setData(testInputString); QCOMPARE(testTextHandler.handleRecievePlainText(Qt::RichText), testOutputString); } void TextHandlerTest::receivePlainTextIn() { const QString testInputString = QStringLiteral("\nTest link https://kde.org."); const QString testOutputStringRich = QStringLiteral("<plain text in tag bracket>
Test link https://kde.org."); QString testOutputStringPlain = QStringLiteral("\nTest link https://kde.org."); // Make sure quotes are maintained in a plain string. const QString testInputString2 = QStringLiteral("last line is \"Time to switch to a new topic.\""); const QString testOutputString2 = QStringLiteral("last line is \"Time to switch to a new topic.\""); TextHandler testTextHandler; testTextHandler.setData(testInputString); QCOMPARE(testTextHandler.handleRecieveRichText(Qt::PlainText), testOutputStringRich); QCOMPARE(testTextHandler.handleRecievePlainText(), testOutputStringPlain); testTextHandler.setData(testInputString2); QCOMPARE(testTextHandler.handleRecieveRichText(Qt::PlainText), testOutputString2); QCOMPARE(testTextHandler.handleRecievePlainText(), testOutputString2); } void TextHandlerTest::receiveStripNewlines() { const QString testInputStringPlain = QStringLiteral("Test\nmany\nnew\nlines."); const QString testInputStringRich = QStringLiteral("Test
many
new
lines."); const QString testOutputString = QStringLiteral("Test many new lines."); const QString testInputStringPlain2 = QStringLiteral("* List\n* Items"); const QString testOutputString2 = QStringLiteral("List Items"); TextHandler testTextHandler; testTextHandler.setData(testInputStringPlain); QCOMPARE(testTextHandler.handleRecievePlainText(Qt::PlainText, true), testOutputString); QCOMPARE(testTextHandler.handleRecieveRichText(Qt::PlainText, nullptr, nullptr, true), testOutputString); testTextHandler.setData(testInputStringRich); QCOMPARE(testTextHandler.handleRecievePlainText(Qt::RichText, true), testOutputString); QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText, nullptr, nullptr, true), testOutputString); testTextHandler.setData(testInputStringPlain2); QCOMPARE(testTextHandler.handleRecievePlainText(Qt::RichText, true), testOutputString2); } /** * For a plain text output of a received string all html is stripped except for * code which is unescaped if it's html. */ void TextHandlerTest::receivePlainStripHtml() { const QString testInputString = QStringLiteral("

Test

Some code with tags
"); const QString testOutputString = QStringLiteral("Test Some code with tags"); TextHandler testTextHandler; testTextHandler.setData(testInputString); QCOMPARE(testTextHandler.handleRecievePlainText(Qt::RichText), testOutputString); } void TextHandlerTest::receivePlainStripMarkup() { const QString testInputString = QStringLiteral("**bold** `

inline code

` *italic*"); const QString testOutputString = QStringLiteral("bold

inline code

italic"); TextHandler testTextHandler; testTextHandler.setData(testInputString); QCOMPARE(testTextHandler.handleRecievePlainText(), testOutputString); } void TextHandlerTest::receiveRichUserPill() { const QString testInputString = QStringLiteral("

@alice:example.org

"); const QString testOutputString = QStringLiteral("

@alice:example.org

"); TextHandler testTextHandler; testTextHandler.setData(testInputString); QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString); } void TextHandlerTest::receiveRichStrikethrough() { const QString testInputString = QStringLiteral("

Test

"); const QString testOutputString = QStringLiteral("

Test

"); TextHandler testTextHandler; testTextHandler.setData(testInputString); QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString); } void TextHandlerTest::receiveRichtextIn() { const QString testInputString = QStringLiteral("

Test

Some code with tags
"); const QString testOutputString = QStringLiteral("

Test

Some code <strong>with tags</strong>
"); TextHandler testTextHandler; testTextHandler.setData(testInputString); QCOMPARE(testTextHandler.handleRecieveRichText(), testOutputString); } #ifdef QUOTIENT_07 void TextHandlerTest::receiveRichMxcUrl() { const QString testInputString = QStringLiteral( "\"image\""); const QString testOutputString = QStringLiteral( ""); TextHandler testTextHandler; testTextHandler.setData(testInputString); QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText, room, room->messageEvents().at(0).get()), testOutputString); } #endif /** * For when your rich input string has a plain text url left in. * * This test is to show that a url that is already rich will be left alone but a * plain one will be linkified. */ void TextHandlerTest::receiveRichPlainUrl() { // This is an actual link that caused trouble which is why it's so long. Keeping // so we can confirm consistent behaviour for complex urls. const QString testInputStringLink1 = QStringLiteral( "https://matrix.to/#/!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&via=matrix.org&via=fedora.im " "Link already rich"); const QString testOutputStringLink1 = QStringLiteral( "https://matrix.to/#/" "!RvzunyTWZGfNxJVQqv:matrix.org/$-9TJVTh5PvW6MvIhFDwteiyLBVGriinueO5eeIazQS8?via=libera.chat&via=matrix.org&via=fedora.im Link already rich"); // Another real case. The linkification wasn't handling it when a single link // contains what looks like and email. It was been broken into 3 but needs to // be just single link. const QString testInputStringLink2 = QStringLiteral("https://lore.kernel.org/lkml/CAHk-=wio46vC4t6xXD-sFqjoPwFm_u515jm3suzmkGxQTeA1_A@mail.gmail.com/"); const QString testOutputStringLink2 = QStringLiteral( "https://lore.kernel.org/lkml/" "CAHk-=wio46vC4t6xXD-sFqjoPwFm_u515jm3suzmkGxQTeA1_A@mail.gmail.com/"); QString testInputStringEmail = QStringLiteral(R"(email@example.com Link already rich)"); QString testOutputStringEmail = QStringLiteral(R"(email@example.com Link already rich)"); QString testInputStringMxId = QStringLiteral("@user:kde.org Link already rich"); QString testOutputStringMxId = QStringLiteral( "@user:kde.org Link already rich"); TextHandler testTextHandler; testTextHandler.setData(testInputStringLink1); QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText), testOutputStringLink1); testTextHandler.setData(testInputStringLink2); QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText), testOutputStringLink2); testTextHandler.setData(testInputStringEmail); QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText), testOutputStringEmail); testTextHandler.setData(testInputStringMxId); QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText), testOutputStringMxId); } // Test that user pill is add to an emote message. // N.B. The second message in the test timeline is marked as an emote. void TextHandlerTest::receiveRichEmote() { auto event = room->messageEvents().at(1).get(); auto author = static_cast(room->user(event->senderId())); const QString testInputString = QStringLiteral("This is an emote."); const QString testOutputString = QStringLiteral("* color().name() + QStringLiteral("\">@example:example.org This is an emote."); TextHandler testTextHandler; testTextHandler.setData(testInputString); QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText, room, event), testOutputString); } void TextHandlerTest::receiveRichEdited_data() { QTest::addColumn("testInputString"); QTest::addColumn("testOutputString"); QTest::newRow("basic") << QStringLiteral("Edited") << QStringLiteral("Edited (edited)"); QTest::newRow("multiple paragraphs") << QStringLiteral("

Edited

\n

Edited

") << QStringLiteral("

Edited

\n

Edited (edited)

"); QTest::newRow("blockquote") << QStringLiteral("
Edited
") << QStringLiteral("
Edited

(edited)

"); } void TextHandlerTest::receiveRichEdited() { QFETCH(QString, testInputString); QFETCH(QString, testOutputString); TextHandler testTextHandler; testTextHandler.setData(testInputString); QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText, room, room->messageEvents().at(2).get()), testOutputString); } void TextHandlerTest::receiveLineSeparator() { auto text = QStringLiteral("foo\u2028bar"); TextHandler textHandler; textHandler.setData(text); QCOMPARE(textHandler.handleRecievePlainText(Qt::PlainText, true), QStringLiteral("foo bar")); } QTEST_MAIN(TextHandlerTest) #include "texthandlertest.moc"