// 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 #include "enums/messagecomponenttype.h" #include "models/customemojimodel.h" #include "neochatconnection.h" #include "testutils.h" using namespace Quotient; class TextHandlerTest : public QObject { Q_OBJECT private: Connection *connection = nullptr; TestUtils::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 sendCustomEmoji(); void sendCustomEmojiCode_data(); void sendCustomEmojiCode(); void receiveSpacelessSelfClosingTag(); 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 receiveRichEdited_data(); void receiveRichEdited(); void receiveLineSeparator(); void receiveRichCodeUrl(); void componentOutput_data(); void componentOutput(); }; void TextHandlerTest::initTestCase() { connection = Connection::makeMockConnection(QStringLiteral("@bob:kde.org")); connection->setAccountData("im.ponies.user_emotes"_ls, QJsonObject{{"images"_ls, QJsonObject{{"test"_ls, QJsonObject{{"body"_ls, "Test custom emoji"_ls}, {"url"_ls, "mxc://example.org/test"_ls}, {"usage"_ls, QJsonArray{"emoticon"_ls}}}}}}}); CustomEmojiModel::instance().setConnection(static_cast(connection)); room = new TestUtils::TestRoom(connection, QStringLiteral("#myroom:kde.org"), QLatin1String("test-texthandler-sync.json")); } 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 left alone."); const QString testOutputString = QStringLiteral("This data should just be left alone."); 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::sendCustomEmoji() { const QString testInputString = QStringLiteral(":test:"); const QString testOutputString = QStringLiteral("\":test:\""); TextHandler testTextHandler; testTextHandler.setData(testInputString); QCOMPARE(testTextHandler.handleSendText(), testOutputString); } void TextHandlerTest::sendCustomEmojiCode_data() { QTest::addColumn("testInputString"); QTest::addColumn("testOutputString"); QTest::newRow("inline") << QStringLiteral("`:test:`") << QStringLiteral(":test:"); QTest::newRow("block") << QStringLiteral("```\n:test:\n```") << QStringLiteral("
:test:\n
"); } // Custom emojis in code blocks should be left alone. void TextHandlerTest::sendCustomEmojiCode() { QFETCH(QString, testInputString); QFETCH(QString, testOutputString); TextHandler testTextHandler; testTextHandler.setData(testInputString); QCOMPARE(testTextHandler.handleSendText(), testOutputString); } void TextHandlerTest::receiveSpacelessSelfClosingTag() { const QString testInputString = QStringLiteral("Test...
...ing"); const QString testRichOutputString = QStringLiteral("Test...
...ing"); const QString testPlainOutputString = QStringLiteral("Test...\n...ing"); TextHandler testTextHandler; testTextHandler.setData(testInputString); QCOMPARE(testTextHandler.handleRecieveRichText(), testRichOutputString); QCOMPARE(testTextHandler.handleRecievePlainText(Qt::RichText), testPlainOutputString); } 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\""); QTest::newRow("new line") << QStringLiteral("new
line") << QStringLiteral("new\nline"); QTest::newRow("unescape") << QStringLiteral("can't") << QStringLiteral("can't"); } 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); } 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); } /** * 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"); QString testInputStringMxIdWithPrefix = QStringLiteral("a @user:kde.org b"); QString testOutputStringMxIdWithPrefix = QStringLiteral("a @user:kde.org b"); 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); testTextHandler.setData(testInputStringMxIdWithPrefix); QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText), testOutputStringMxIdWithPrefix); } 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)

"); } void TextHandlerTest::receiveRichEdited() { QFETCH(QString, testInputString); QFETCH(QString, testOutputString); TextHandler testTextHandler; testTextHandler.setData(testInputString); const auto event = eventCast(room->messageEvents().at(2).get()); QCOMPARE(testTextHandler.handleRecieveRichText(Qt::RichText, room, event, false, event->isReplaced()), testOutputString); } void TextHandlerTest::receiveLineSeparator() { auto text = QStringLiteral("foo\u2028bar"); TextHandler textHandler; textHandler.setData(text); QCOMPARE(textHandler.handleRecievePlainText(Qt::PlainText, true), QStringLiteral("foo bar")); } void TextHandlerTest::receiveRichCodeUrl() { auto input = QStringLiteral("https://kde.org"); TextHandler testTextHandler; testTextHandler.setData(input); QCOMPARE(testTextHandler.handleRecieveRichText(), input); } void TextHandlerTest::componentOutput_data() { QTest::addColumn("testInputString"); QTest::addColumn>("testOutputComponents"); QTest::newRow("multiple paragraphs") << QStringLiteral("

Text

\n

Text

") << QList{MessageComponent{MessageComponentType::Text, QStringLiteral("Text"), {}}, MessageComponent{MessageComponentType::Text, QStringLiteral("Text"), {}}}; QTest::newRow("code") << QStringLiteral("

Text

\n
Some code\n
") << QList{MessageComponent{MessageComponentType::Text, QStringLiteral("Text"), {}}, MessageComponent{MessageComponentType::Code, QStringLiteral("Some code"), QVariantMap{{QStringLiteral("class"), QStringLiteral("html")}}}}; QTest::newRow("quote") << QStringLiteral("

Text

\n
\n

blockquote

\n
") << QList{MessageComponent{MessageComponentType::Text, QStringLiteral("Text"), {}}, MessageComponent{MessageComponentType::Quote, QStringLiteral("\"blockquote\""), {}}}; QTest::newRow("no tag first paragraph") << QStringLiteral("Text\n

Text

") << QList{MessageComponent{MessageComponentType::Text, QStringLiteral("Text"), {}}, MessageComponent{MessageComponentType::Text, QStringLiteral("Text"), {}}}; QTest::newRow("no tag last paragraph") << QStringLiteral("

Text

\nText") << QList{MessageComponent{MessageComponentType::Text, QStringLiteral("Text"), {}}, MessageComponent{MessageComponentType::Text, QStringLiteral("Text"), {}}}; QTest::newRow("inline code") << QStringLiteral("

https://kde.org

\n

Text

") << QList{MessageComponent{MessageComponentType::Text, QStringLiteral("https://kde.org"), {}}, MessageComponent{MessageComponentType::Text, QStringLiteral("Text"), {}}}; QTest::newRow("inline code single block") << QStringLiteral("https://kde.org") << QList{ MessageComponent{MessageComponentType::Text, QStringLiteral("https://kde.org"), {}}}; QTest::newRow("long start tag") << QStringLiteral( "Ah, you mean something like
# main.qml\nimport CustomQml\n...\nControls.TextField { id: "
               "someField }\nCustomQml {\n    someTextProperty: someField.text\n}\n
Sure you can, it's still local to the same file where you " "defined the id") << QList{ MessageComponent{MessageComponentType::Text, QStringLiteral("Ah, you mean something like
"), {}}, MessageComponent{ MessageComponentType::Code, QStringLiteral( "# main.qml\nimport CustomQml\n...\nControls.TextField { id: someField }\nCustomQml {\n someTextProperty: someField.text\n}"), QVariantMap{{QStringLiteral("class"), QStringLiteral("qml")}}}, MessageComponent{MessageComponentType::Text, QStringLiteral("Sure you can, it's still local to the same file where you defined the id"), {}}}; } void TextHandlerTest::componentOutput() { QFETCH(QString, testInputString); QFETCH(QList, testOutputComponents); TextHandler testTextHandler; QCOMPARE(testTextHandler.textComponents(testInputString), testOutputComponents); } QTEST_MAIN(TextHandlerTest) #include "texthandlertest.moc"